From 416b2279e212810d1e4074543e622c67170a8455 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Thu, 18 Apr 2024 22:24:17 -0500 Subject: [PATCH 01/62] Start setup of tab system --- ui/common.slint | 11 +++- ui/editmod.slint | 6 +++ ui/main.slint | 1 + ui/tab_bar.slint | 130 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 ui/tab_bar.slint diff --git a/ui/common.slint b/ui/common.slint index 10a14b0..942ce38 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -76,7 +76,7 @@ export component Page inherits Rectangle { in property title: "title"; in property description: "description"; in property has-back-button; - width: 310px; + width: 315px; background: ColorPalette.page-background-color; callback back; @@ -171,5 +171,14 @@ export component Page inherits Rectangle { } } } + @children +} + +export component Tab inherits Rectangle { + background: ColorPalette.page-background-color; + width: 315px; + + TouchArea {} // Protect underneath controls + @children } \ No newline at end of file diff --git a/ui/editmod.slint b/ui/editmod.slint index 9c64923..8ef50d1 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -1,5 +1,6 @@ import { GroupBox, Button, ScrollView } from "std-widgets.slint"; import { MainLogic, SettingsLogic, Page } from "common.slint"; +import { TabBar } from "tab_bar.slint"; export component ModDetailsPage inherits Page { has-back-button: true; @@ -26,6 +27,11 @@ export component ModDetailsPage inherits Page { font-size: 10pt; text: @tr("Enabled:"); } + + side-bar := TabBar { + model: [@tr("Menu" => "Details"), @tr("Menu" => "Edit")]; + } + b := HorizontalLayout { padding-left: 8px; padding-top: 3px; diff --git a/ui/main.slint b/ui/main.slint index e1dd085..19aad6d 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -14,6 +14,7 @@ export component MainPage inherits Page { callback edit-mod(int); focus-line-edit => { input-mod.focus() } edit-mod(i) => { + // mod-settings.tab = 0; mod-settings.mod-index = i; MainLogic.current-subpage = 2 } diff --git a/ui/tab_bar.slint b/ui/tab_bar.slint new file mode 100644 index 0000000..ea77d02 --- /dev/null +++ b/ui/tab_bar.slint @@ -0,0 +1,130 @@ +import { ColorPalette } from "common.slint"; + +component TabItem inherits Rectangle { + in property selected; + in property has-focus; + in-out property text <=> label.text; + + callback clicked <=> touch.clicked; + + min-height: l.preferred-height; + + states [ + pressed when touch.pressed : { + state.opacity: 0.8; + } + hover when touch.has-hover : { + state.opacity: 0.6; + } + selected when root.selected : { + state.opacity: 1; + } + focused when root.has-focus : { + state.opacity: 0.8; + } + ] + + state := Rectangle { + opacity: 0; + background: ColorPalette.page-background-color; + + animate opacity { duration: 150ms; } + } + + l := HorizontalLayout { + y: (parent.height - self.height) / 2; + padding: 3px; + spacing: 0px; + + label := Text { + color: ColorPalette.text-base; + vertical-alignment: center; + } + } + + touch := TouchArea { + width: 100%; + height: 100%; + } +} + +export component TabBar inherits Rectangle { + in property <[string]> model: []; + out property current-item: 0; + out property current-focused: fs.has-focus ? fs.focused-tab : -1; // The currently focused tab + + width: 315px; + height: 30px; + forward-focus: fs; + accessible-role: tab; + accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-item; + + Rectangle { + background: ColorPalette.page-background-color.darker(0.2); + + fs := FocusScope { + key-pressed(event) => { + if (event.text == "\n") { + root.current-item = root.current-focused; + return accept; + } + if (event.text == Key.UpArrow) { + self.focused-tab = Math.max(self.focused-tab - 1, 0); + return accept; + } + if (event.text == Key.DownArrow) { + self.focused-tab = Math.min(self.focused-tab + 1, root.model.length - 1); + return accept; + } + return reject; + } + + key-released(event) => { + if (event.text == " ") { + root.current-item = root.current-focused; + return accept; + } + return reject; + } + + property focused-tab: 0; + + x: 0; + width: 0; // Do not react on clicks + } + } + + HorizontalLayout { + // consider making padding and spacing elements defined in common.slint + padding-top: 3px; + padding-bottom: 3px; + spacing: 3px; + alignment: start; + + label := Text { + font-size: 16px; + horizontal-alignment: center; + } + + navigation := HorizontalLayout { + alignment: start; + vertical-stretch: 0; + for item[index] in root.model : TabItem { + clicked => { root.current-item = index; } + + has-focus: index == root.current-focused; + text: item; + selected: index == root.current-item; + } + } + + HorizontalLayout { + bottom := HorizontalLayout { + padding-left: 3px; + padding-right: 3px; + + @children + } + } + } +} \ No newline at end of file From 71f3b4366d75de69f824b1f18342e528add02d25 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 21 Apr 2024 17:41:37 -0500 Subject: [PATCH 02/62] Working tab bar! Now have tabs to display in the editmod.slint page Tab for displaying details about selected mod Tab for editing selected mod More UI work has to be done, going to clean up files into list widget have to add new features to the edit mod tab --- ui/appwindow.slint | 8 +- ui/common.slint | 14 ++- ui/editmod.slint | 135 ++++++------------------- ui/main.slint | 8 +- ui/{subpages.slint => sub-pages.slint} | 0 ui/tab-bar.slint | 79 +++++++++++++++ ui/tab_bar.slint | 130 ------------------------ ui/tabs.slint | 91 +++++++++++++++++ 8 files changed, 222 insertions(+), 243 deletions(-) rename ui/{subpages.slint => sub-pages.slint} (100%) create mode 100644 ui/tab-bar.slint delete mode 100644 ui/tab_bar.slint create mode 100644 ui/tabs.slint diff --git a/ui/appwindow.slint b/ui/appwindow.slint index fe3ca4b..e44487f 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -1,5 +1,5 @@ import { MainPage } from "main.slint"; -import { MainLogic, SettingsLogic, DisplayMod, ColorPalette, Message } from "common.slint"; +import { MainLogic, SettingsLogic, DisplayMod, ColorPalette, Message, Formating } from "common.slint"; import { StandardButton } from "std-widgets.slint"; export { MainLogic, SettingsLogic, DisplayMod } @@ -13,7 +13,7 @@ export component App inherits Window { // popup-window-width = text-width + dialog boarder property popup-window-width: msg-size.width + 13px; // window-height = main-page-height -? page-title-height - property window-height: mp.height - 48px; + property window-height: mp.height - Formating.header-height; property popup-window-x-pos: { if ((mp.width - popup-window-width) / 2) < 10px { 10px @@ -35,8 +35,8 @@ export component App inherits Window { icon: @image-url("assets/EML-icon.png"); // preferred-height: 381px; min-height: 381px; - min-width: 315px; - max-width: 315px; + min-width: Formating.app-width; + max-width: Formating.app-width; mp := MainPage {} diff --git a/ui/common.slint b/ui/common.slint index 942ce38..dd1cfd4 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -72,11 +72,20 @@ export global ColorPalette { }; } +export global Formating { + out property app-width: 315px; + out property header-height: 48px; + out property tab-bar-height: 30px; + out property layout-width: app-width - 10px; + out property default-padding: 3px; + out property default-spacing: 3px; +} + export component Page inherits Rectangle { in property title: "title"; in property description: "description"; in property has-back-button; - width: 315px; + width: Formating.app-width; background: ColorPalette.page-background-color; callback back; @@ -89,7 +98,7 @@ export component Page inherits Rectangle { HorizontalLayout { x: 0; y: 0; - height: 48px; + height: Formating.header-height; padding-left: 5px; padding-right: 8px; padding-top: 8px; @@ -176,6 +185,7 @@ export component Page inherits Rectangle { export component Tab inherits Rectangle { background: ColorPalette.page-background-color; + y: Formating.header-height + Formating.tab-bar-height; width: 315px; TouchArea {} // Protect underneath controls diff --git a/ui/editmod.slint b/ui/editmod.slint index 8ef50d1..57c29c1 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -1,119 +1,48 @@ import { GroupBox, Button, ScrollView } from "std-widgets.slint"; -import { MainLogic, SettingsLogic, Page } from "common.slint"; -import { TabBar } from "tab_bar.slint"; +import { MainLogic, SettingsLogic, Page, Formating, Tab } from "common.slint"; +import { TabBar } from "tab-bar.slint"; +import { ModDetails, ModEdit } from "tabs.slint"; export component ModDetailsPage inherits Page { has-back-button: true; title: MainLogic.current-mods[mod-index].name; description: @tr("Edit registered mods here"); + // height: 400px; in property mod-index; - property box-width: root.width - 10px; + in-out property current-tab; property state-color: SettingsLogic.loader-disabled ? #d01616 : - MainLogic.current-mods[mod-index].enabled ? #206816 : #d01616; + MainLogic.current-mods[mod-index].enabled ? #206816 : #d01616; property state: SettingsLogic.loader-disabled ? @tr("Mod Loader Disabled") : - MainLogic.current-mods[mod-index].enabled ? @tr("Yes") : @tr("No"); - property button-width: MainLogic.current-mods[mod-index].has-config ? 93px : 105px; - property button-layout: MainLogic.current-mods[mod-index].has-config ? center : end; + MainLogic.current-mods[mod-index].enabled ? @tr("Mod Enabled") : @tr("Mod Disabled"); - details := VerticalLayout { - y: 45px; - height: root.height - edit-mod-box.height - 55px; - padding-top: 4px; - padding-left: 12px; - padding-right: 12px; - alignment: start; - a := Text { - font-size: 10pt; - text: @tr("Enabled:"); + info-text := HorizontalLayout { + y: Formating.header-height - 10px; + height: 25px; + padding-right: 10px; + Text { + font-size: 16pt; + color: state-color; + text: state; + horizontal-alignment: right; } - - side-bar := TabBar { - model: [@tr("Menu" => "Details"), @tr("Menu" => "Edit")]; - } - - b := HorizontalLayout { - padding-left: 8px; - padding-top: 3px; - padding-bottom: 3px; - Text { - font-size: 16pt; - color: state-color; - text: state; - } - } - c := Text { - font-size: 10pt; - text: @tr("Name:"); - } - d := HorizontalLayout { - padding-left: 8px; - padding-top: 3px; - padding-bottom: 3px; - Text { - font-size: 13pt; - wrap: word-wrap; - text: MainLogic.current-mods[mod-index].name; - } - } - e := Text { - font-size: 10pt; - text: @tr("Files:"); - } - - } - ScrollView { - y: details.y + a.height + b.height + c.height + d.height + e.height + 9px; - height: root.height - self.y - edit-mod-box.height + 3px; - width: box-width; - viewport-height: files-txt.height; - viewport-width: files-txt.width; - files-txt := Text { - x: 15px; - width: parent.width - self.x; - font-size: 13pt; - wrap: word-wrap; - text: MainLogic.current-mods[mod-index].files; - } + + tab-bar := TabBar { + y: Formating.header-height + 20px; + model: [@tr("Tab" => "Details"), @tr("Tab" => "Edit")]; + current-item <=> current-tab; } - VerticalLayout { - y: root.height - edit-mod-box.height; - height: root.height - self.y; - padding-left: 8px; - alignment: end; - - edit-mod-box := GroupBox { - title: @tr("Edit Mod"); - height: 95px; - HorizontalLayout { - width: box-width; - spacing: 7px; - padding-right: 8px; - padding-bottom: 8px; - alignment: button-layout; - Button { - width: button-width; - height: 35px; - primary: !SettingsLogic.dark-mode; - text: @tr("Add Files"); - clicked => { MainLogic.add-to-mod(MainLogic.current-mods[mod-index].name) } - } - if (MainLogic.current-mods[mod-index].has-config) : Button { - width: button-width; - height: 35px; - primary: !SettingsLogic.dark-mode; - text: @tr("Edit config"); - clicked => { MainLogic.edit-config(MainLogic.current-mods[mod-index].config-files) } - } - Button { - width: button-width; - height: 35px; - primary: !SettingsLogic.dark-mode; - text: @tr("De-register"); - clicked => { MainLogic.remove-mod(MainLogic.current-mods[mod-index].name) } - } - } - } + + if(tab-bar.current-item == 0) : ModDetails { + mod-index: mod-index; + height: root.height - Formating.header-height - info-text.height - tab-bar.height; + y: Formating.header-height + tab-bar.height + info-text.height; } + if(tab-bar.current-item == 1) : ModEdit { + mod-index: mod-index; + height: root.height - Formating.header-height - info-text.height - tab-bar.height; + y: Formating.header-height + tab-bar.height + info-text.height; + } + } \ No newline at end of file diff --git a/ui/main.slint b/ui/main.slint index 19aad6d..af17d5e 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -1,6 +1,6 @@ import { CheckBox, GroupBox, ListView, LineEdit, Button } from "std-widgets.slint"; -import { SettingsPage, ModDetailsPage } from "subpages.slint"; -import { MainLogic, SettingsLogic, Page, ColorPalette } from "common.slint"; +import { SettingsPage, ModDetailsPage } from "sub-pages.slint"; +import { MainLogic, SettingsLogic, Page, ColorPalette, Formating } from "common.slint"; export component MainPage inherits Page { has-back-button: false; @@ -14,7 +14,7 @@ export component MainPage inherits Page { callback edit-mod(int); focus-line-edit => { input-mod.focus() } edit-mod(i) => { - // mod-settings.tab = 0; + mod-settings.current-tab = 0; mod-settings.mod-index = i; MainLogic.current-subpage = 2 } @@ -22,7 +22,7 @@ export component MainPage inherits Page { VerticalLayout { y: 27px; height: parent.height - self.y; - preferred-width: 460px; + preferred-width: Formating.app-width; padding: 8px; reg-mod-box := GroupBox { diff --git a/ui/subpages.slint b/ui/sub-pages.slint similarity index 100% rename from ui/subpages.slint rename to ui/sub-pages.slint diff --git a/ui/tab-bar.slint b/ui/tab-bar.slint new file mode 100644 index 0000000..3db2a5d --- /dev/null +++ b/ui/tab-bar.slint @@ -0,0 +1,79 @@ +import { ColorPalette, Formating } from "common.slint"; + +component TabItem inherits Rectangle { + in property selected; + in-out property text <=> label.text; + + callback clicked <=> touch.clicked; + + height: Formating.tab-bar-height; + width: Formating.app-width / 2; + + states [ + pressed when touch.pressed : { + state.opacity: 0.8; + } + hover when touch.has-hover : { + state.opacity: 0.6; + } + selected when root.selected : { + state.opacity: 1; + } + ] + + state := Rectangle { + opacity: 0; + background: ColorPalette.page-background-color; + border-top-left-radius: 13px; + border-top-right-radius: 13px; + + animate opacity { duration: 150ms; } + } + + HorizontalLayout { + y: (parent.height - self.height) / 2; + height: parent.height; + padding: Formating.default-padding; + padding-right: 8px; + spacing: 0px; + + label := Text { + color: ColorPalette.text-base; + font-size: 14px; + vertical-alignment: center; + horizontal-alignment: right; + } + } + + touch := TouchArea { + width: 100%; + height: 100%; + } +} + +export component TabBar inherits Rectangle { + in property <[string]> model: []; + in-out property current-item: 0; + + background: ColorPalette.page-background-color.darker(0.2); + border-top-left-radius: 10px; + border-top-right-radius: 10px; + width: Formating.app-width; + height: Formating.tab-bar-height; + + HorizontalLayout { + padding-top: Formating.default-padding; + spacing: Formating.default-spacing; + alignment: start; + + navigation := HorizontalLayout { + alignment: start; + vertical-stretch: 0; + for item[index] in root.model : TabItem { + clicked => { root.current-item = index; } + text: item; + selected: index == root.current-item; + } + } + } +} \ No newline at end of file diff --git a/ui/tab_bar.slint b/ui/tab_bar.slint deleted file mode 100644 index ea77d02..0000000 --- a/ui/tab_bar.slint +++ /dev/null @@ -1,130 +0,0 @@ -import { ColorPalette } from "common.slint"; - -component TabItem inherits Rectangle { - in property selected; - in property has-focus; - in-out property text <=> label.text; - - callback clicked <=> touch.clicked; - - min-height: l.preferred-height; - - states [ - pressed when touch.pressed : { - state.opacity: 0.8; - } - hover when touch.has-hover : { - state.opacity: 0.6; - } - selected when root.selected : { - state.opacity: 1; - } - focused when root.has-focus : { - state.opacity: 0.8; - } - ] - - state := Rectangle { - opacity: 0; - background: ColorPalette.page-background-color; - - animate opacity { duration: 150ms; } - } - - l := HorizontalLayout { - y: (parent.height - self.height) / 2; - padding: 3px; - spacing: 0px; - - label := Text { - color: ColorPalette.text-base; - vertical-alignment: center; - } - } - - touch := TouchArea { - width: 100%; - height: 100%; - } -} - -export component TabBar inherits Rectangle { - in property <[string]> model: []; - out property current-item: 0; - out property current-focused: fs.has-focus ? fs.focused-tab : -1; // The currently focused tab - - width: 315px; - height: 30px; - forward-focus: fs; - accessible-role: tab; - accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-item; - - Rectangle { - background: ColorPalette.page-background-color.darker(0.2); - - fs := FocusScope { - key-pressed(event) => { - if (event.text == "\n") { - root.current-item = root.current-focused; - return accept; - } - if (event.text == Key.UpArrow) { - self.focused-tab = Math.max(self.focused-tab - 1, 0); - return accept; - } - if (event.text == Key.DownArrow) { - self.focused-tab = Math.min(self.focused-tab + 1, root.model.length - 1); - return accept; - } - return reject; - } - - key-released(event) => { - if (event.text == " ") { - root.current-item = root.current-focused; - return accept; - } - return reject; - } - - property focused-tab: 0; - - x: 0; - width: 0; // Do not react on clicks - } - } - - HorizontalLayout { - // consider making padding and spacing elements defined in common.slint - padding-top: 3px; - padding-bottom: 3px; - spacing: 3px; - alignment: start; - - label := Text { - font-size: 16px; - horizontal-alignment: center; - } - - navigation := HorizontalLayout { - alignment: start; - vertical-stretch: 0; - for item[index] in root.model : TabItem { - clicked => { root.current-item = index; } - - has-focus: index == root.current-focused; - text: item; - selected: index == root.current-item; - } - } - - HorizontalLayout { - bottom := HorizontalLayout { - padding-left: 3px; - padding-right: 3px; - - @children - } - } - } -} \ No newline at end of file diff --git a/ui/tabs.slint b/ui/tabs.slint new file mode 100644 index 0000000..f587c65 --- /dev/null +++ b/ui/tabs.slint @@ -0,0 +1,91 @@ +import { ScrollView, GroupBox, Button } from "std-widgets.slint"; +import { Tab, SettingsLogic, MainLogic, Formating } from "common.slint"; + +export component ModDetails inherits Tab { + in property mod-index; + details := VerticalLayout { + y: 0px; + padding-top: Formating.default-padding; + padding-left: 12px; + padding-right: 12px; + alignment: start; + + a := Text { + font-size: 10pt; + text: @tr("Name:"); + } + b := HorizontalLayout { + padding-left: 8px; + padding-top: Formating.default-padding; + padding-bottom: Formating.default-padding; + Text { + font-size: 13pt; + wrap: word-wrap; + text: MainLogic.current-mods[mod-index].name; + } + } + c := Text { + font-size: 10pt; + text: @tr("Files:"); + } + } + ScrollView { + y: details.y + a.height + b.height + c.height + 9px; + height: root.height - 75px; + width: Formating.layout-width; + viewport-height: files-txt.height; + viewport-width: files-txt.width; + files-txt := Text { + x: 15px; + width: parent.width - self.x; + font-size: 13pt; + wrap: word-wrap; + text: MainLogic.current-mods[mod-index].files; + } + } +} + +export component ModEdit inherits Tab { + in property mod-index; + property button-width: MainLogic.current-mods[mod-index].has-config ? 93px : 105px; + property button-layout: MainLogic.current-mods[mod-index].has-config ? center : end; + VerticalLayout { + y: root.height - edit-mod-box.height; + height: root.height - self.y; + padding-left: 8px; + alignment: end; + + edit-mod-box := GroupBox { + title: @tr("Edit Mod"); + height: 95px; + HorizontalLayout { + width: Formating.layout-width; + spacing: 7px; + padding-right: 8px; + padding-bottom: 8px; + alignment: button-layout; + Button { + width: button-width; + height: 35px; + primary: !SettingsLogic.dark-mode; + text: @tr("Add Files"); + clicked => { MainLogic.add-to-mod(MainLogic.current-mods[mod-index].name) } + } + if (MainLogic.current-mods[mod-index].has-config) : Button { + width: button-width; + height: 35px; + primary: !SettingsLogic.dark-mode; + text: @tr("Edit config"); + clicked => { MainLogic.edit-config(MainLogic.current-mods[mod-index].config-files) } + } + Button { + width: button-width; + height: 35px; + primary: !SettingsLogic.dark-mode; + text: @tr("De-register"); + clicked => { MainLogic.remove-mod(MainLogic.current-mods[mod-index].name) } + } + } + } + } +} \ No newline at end of file From d45394e42a717941226b4105e2a1ab2876167815 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 21 Apr 2024 22:11:54 -0500 Subject: [PATCH 03/62] Lots of UI cleanup Moved lots of length values into Formatting in common.slint This cleans up the slint code and makes sure pages look more stylized --- Cargo.lock | 94 +++++++++++++++++++++++----------------------- ui/appwindow.slint | 12 +++--- ui/common.slint | 17 ++++++--- ui/editmod.slint | 24 +++++++----- ui/main.slint | 11 +++--- ui/settings.slint | 65 ++++++++++++++------------------ ui/tab-bar.slint | 24 ++++++------ ui/tabs.slint | 30 +++++++-------- 8 files changed, 142 insertions(+), 135 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6025bb..76333d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,7 +103,7 @@ dependencies = [ "accesskit_macos", "accesskit_unix", "accesskit_windows", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "winit", ] @@ -371,7 +371,7 @@ dependencies = [ "futures-lite 2.3.0", "parking", "polling 3.6.0", - "rustix 0.38.32", + "rustix 0.38.33", "slab", "tracing", "windows-sys 0.52.0", @@ -421,15 +421,15 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.32", + "rustix 0.38.33", "windows-sys 0.48.0", ] [[package]] name = "async-process" -version = "2.2.1" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad07b3443bfa10dcddf86a452ec48949e8e7fedf7392d82de3969fda99e90ed" +checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" dependencies = [ "async-channel", "async-io 2.3.2", @@ -440,7 +440,7 @@ dependencies = [ "cfg-if", "event-listener 5.3.0", "futures-lite 2.3.0", - "rustix 0.38.32", + "rustix 0.38.33", "tracing", "windows-sys 0.52.0", ] @@ -458,20 +458,20 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" dependencies = [ "async-io 2.3.2", - "async-lock 2.8.0", + "async-lock 3.3.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix 0.38.32", + "rustix 0.38.33", "signal-hook-registry", "slab", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -738,7 +738,7 @@ dependencies = [ "bitflags 2.5.0", "log", "polling 3.6.0", - "rustix 0.38.32", + "rustix 0.38.33", "slab", "thiserror", ] @@ -750,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" dependencies = [ "calloop", - "rustix 0.38.32", + "rustix 0.38.33", "wayland-backend", "wayland-client", ] @@ -763,12 +763,13 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" dependencies = [ "jobserver", "libc", + "once_cell", ] [[package]] @@ -2673,11 +2674,12 @@ dependencies = [ [[package]] name = "lyon_extra" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ce2ae38f2480094ec1f0d5df51a75581fa84f0e8f32a0edb1d264630c99f3b" +checksum = "8c4a243ce61e7e5f3ae6c72a88d8fb081b6c69f13500c15e99cfd1159a833b20" dependencies = [ "lyon_path", + "thiserror", ] [[package]] @@ -2795,7 +2797,7 @@ dependencies = [ "ndk-sys", "num_enum", "raw-window-handle 0.5.2", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "thiserror", ] @@ -3177,7 +3179,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.32", + "rustix 0.38.33", "tracing", "windows-sys 0.52.0", ] @@ -3317,9 +3319,9 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "raw-window-handle" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a9830a0e1b9fb145ebb365b8bc4ccd75f290f98c0247deafbbe2c75cefb544" +checksum = "8cc3bcbdb1ddfc11e700e62968e6b4cc9c75bb466464ad28fb61c5b2c964418b" [[package]] name = "rayon" @@ -3417,7 +3419,7 @@ dependencies = [ "objc-foundation", "objc_id", "pollster", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "urlencoding", "wasm-bindgen", "wasm-bindgen-futures", @@ -3501,9 +3503,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" dependencies = [ "bitflags 2.5.0", "errno", @@ -3672,9 +3674,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -3765,7 +3767,7 @@ dependencies = [ "i-slint-compiler", "spin_on", "thiserror", - "toml_edit 0.22.11", + "toml_edit 0.22.12", ] [[package]] @@ -3808,7 +3810,7 @@ dependencies = [ "libc", "log", "memmap2 0.9.4", - "rustix 0.38.32", + "rustix 0.38.33", "thiserror", "wayland-backend", "wayland-client", @@ -3869,7 +3871,7 @@ dependencies = [ "objc", "raw-window-handle 0.5.2", "redox_syscall 0.4.1", - "rustix 0.38.32", + "rustix 0.38.33", "tiny-xlib", "wasm-bindgen", "wayland-backend", @@ -3992,7 +3994,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.2", - "rustix 0.38.32", + "rustix 0.38.33", "windows-sys 0.52.0", ] @@ -4013,18 +4015,18 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" dependencies = [ "proc-macro2", "quote", @@ -4145,7 +4147,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.11", + "toml_edit 0.22.12", ] [[package]] @@ -4183,9 +4185,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.11" +version = "0.22.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb686a972ccef8537b39eead3968b0e8616cb5040dbb9bba93007c8e07c9215f" +checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" dependencies = [ "indexmap", "serde", @@ -4522,7 +4524,7 @@ checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.32", + "rustix 0.38.33", "scoped-tls", "smallvec", "wayland-sys", @@ -4535,7 +4537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ "bitflags 2.5.0", - "rustix 0.38.32", + "rustix 0.38.33", "wayland-backend", "wayland-scanner", ] @@ -4557,7 +4559,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" dependencies = [ - "rustix 0.38.32", + "rustix 0.38.33", "wayland-client", "xcursor", ] @@ -4668,7 +4670,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.32", + "rustix 0.38.33", ] [[package]] @@ -5016,9 +5018,9 @@ dependencies = [ "orbclient", "percent-encoding", "raw-window-handle 0.5.2", - "raw-window-handle 0.6.0", + "raw-window-handle 0.6.1", "redox_syscall 0.3.5", - "rustix 0.38.32", + "rustix 0.38.33", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -5123,7 +5125,7 @@ dependencies = [ "libc", "libloading 0.8.3", "once_cell", - "rustix 0.38.32", + "rustix 0.38.33", "x11rb-protocol 0.13.0", ] @@ -5150,7 +5152,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.13", - "rustix 0.38.32", + "rustix 0.38.33", ] [[package]] @@ -5263,7 +5265,7 @@ dependencies = [ "async-fs 2.1.1", "async-io 2.3.2", "async-lock 3.3.0", - "async-process 2.2.1", + "async-process 2.2.2", "async-recursion", "async-task", "async-trait", diff --git a/ui/appwindow.slint b/ui/appwindow.slint index e44487f..796d306 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -1,5 +1,5 @@ import { MainPage } from "main.slint"; -import { MainLogic, SettingsLogic, DisplayMod, ColorPalette, Message, Formating } from "common.slint"; +import { MainLogic, SettingsLogic, DisplayMod, ColorPalette, Message, Formatting } from "common.slint"; import { StandardButton } from "std-widgets.slint"; export { MainLogic, SettingsLogic, DisplayMod } @@ -13,7 +13,7 @@ export component App inherits Window { // popup-window-width = text-width + dialog boarder property popup-window-width: msg-size.width + 13px; // window-height = main-page-height -? page-title-height - property window-height: mp.height - Formating.header-height; + property window-height: mp.height - Formatting.header-height; property popup-window-x-pos: { if ((mp.width - popup-window-width) / 2) < 10px { 10px @@ -33,10 +33,10 @@ export component App inherits Window { // property debug-msg: "height calc: " + popup-window-y-pos / 1px + "\nPage Height: " + debug-mp-height / 1px + "\nDialog Height: " + popup-window-height / 1px; title: @tr("Elden Mod Loader"); icon: @image-url("assets/EML-icon.png"); - // preferred-height: 381px; - min-height: 381px; - min-width: Formating.app-width; - max-width: Formating.app-width; + preferred-height: Formatting.app-preferred-height; + min-height: Formatting.app-preferred-height; + min-width: Formatting.app-width; + max-width: Formatting.app-width; mp := MainPage {} diff --git a/ui/common.slint b/ui/common.slint index dd1cfd4..9b87f25 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -72,21 +72,27 @@ export global ColorPalette { }; } -export global Formating { +export global Formatting { out property app-width: 315px; + out property app-preferred-height: 381px; out property header-height: 48px; out property tab-bar-height: 30px; out property layout-width: app-width - 10px; out property default-padding: 3px; out property default-spacing: 3px; + out property side-spacing: 8px; + out property button-spacing: 5px; + out property rectangle-radius: 10px; + out property group-box-width: app-width - Formatting.side-spacing; } export component Page inherits Rectangle { in property title: "title"; in property description: "description"; in property has-back-button; - width: Formating.app-width; - background: ColorPalette.page-background-color; + in property dark-header; + width: Formatting.app-width; + background: dark-header ? ColorPalette.page-background-color.darker(0.16) : ColorPalette.page-background-color; callback back; callback settings; @@ -98,7 +104,7 @@ export component Page inherits Rectangle { HorizontalLayout { x: 0; y: 0; - height: Formating.header-height; + height: Formatting.header-height; padding-left: 5px; padding-right: 8px; padding-top: 8px; @@ -185,8 +191,7 @@ export component Page inherits Rectangle { export component Tab inherits Rectangle { background: ColorPalette.page-background-color; - y: Formating.header-height + Formating.tab-bar-height; - width: 315px; + width: Formatting.app-width; TouchArea {} // Protect underneath controls diff --git a/ui/editmod.slint b/ui/editmod.slint index 57c29c1..781bbdb 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -1,12 +1,15 @@ import { GroupBox, Button, ScrollView } from "std-widgets.slint"; -import { MainLogic, SettingsLogic, Page, Formating, Tab } from "common.slint"; +import { MainLogic, SettingsLogic, Page, Tab, Formatting, ColorPalette } from "common.slint"; import { TabBar } from "tab-bar.slint"; import { ModDetails, ModEdit } from "tabs.slint"; export component ModDetailsPage inherits Page { + dark-header: true; has-back-button: true; title: MainLogic.current-mods[mod-index].name; description: @tr("Edit registered mods here"); + // values for live preview editing - This will mess + // ------------up alignment if left on------------- // height: 400px; in property mod-index; @@ -15,34 +18,37 @@ export component ModDetailsPage inherits Page { MainLogic.current-mods[mod-index].enabled ? #206816 : #d01616; property state: SettingsLogic.loader-disabled ? @tr("Mod Loader Disabled") : MainLogic.current-mods[mod-index].enabled ? @tr("Mod Enabled") : @tr("Mod Disabled"); + property header-offset: 9px; info-text := HorizontalLayout { - y: Formating.header-height - 10px; - height: 25px; - padding-right: 10px; + y: Formatting.header-height - header-offset; + width: Formatting.app-width; + height: 27px; + padding-right: 8px; Text { font-size: 16pt; color: state-color; text: state; horizontal-alignment: right; } + } tab-bar := TabBar { - y: Formating.header-height + 20px; + y: Formatting.header-height + 15px; model: [@tr("Tab" => "Details"), @tr("Tab" => "Edit")]; current-item <=> current-tab; } if(tab-bar.current-item == 0) : ModDetails { mod-index: mod-index; - height: root.height - Formating.header-height - info-text.height - tab-bar.height; - y: Formating.header-height + tab-bar.height + info-text.height; + height: root.height - Formatting.header-height - info-text.height - tab-bar.height + header-offset; + y: Formatting.header-height + tab-bar.height + info-text.height - header-offset; } if(tab-bar.current-item == 1) : ModEdit { mod-index: mod-index; - height: root.height - Formating.header-height - info-text.height - tab-bar.height; - y: Formating.header-height + tab-bar.height + info-text.height; + height: root.height - Formatting.header-height - info-text.height - tab-bar.height + header-offset; + y: Formatting.header-height + tab-bar.height + info-text.height - header-offset; } } \ No newline at end of file diff --git a/ui/main.slint b/ui/main.slint index af17d5e..c1952c2 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -1,6 +1,6 @@ import { CheckBox, GroupBox, ListView, LineEdit, Button } from "std-widgets.slint"; import { SettingsPage, ModDetailsPage } from "sub-pages.slint"; -import { MainLogic, SettingsLogic, Page, ColorPalette, Formating } from "common.slint"; +import { MainLogic, SettingsLogic, Page, ColorPalette, Formatting } from "common.slint"; export component MainPage inherits Page { has-back-button: false; @@ -22,8 +22,9 @@ export component MainPage inherits Page { VerticalLayout { y: 27px; height: parent.height - self.y; - preferred-width: Formating.app-width; - padding: 8px; + preferred-width: Formatting.app-width; + padding: Formatting.side-spacing; + padding-bottom: Formatting.side-spacing / 2; reg-mod-box := GroupBox { title: @tr("Registered-Mods:"); @@ -32,7 +33,7 @@ export component MainPage inherits Page { list-view := ListView { for mod[idx] in MainLogic.current-mods: re := Rectangle { height: 31px; - border-radius: 10px; + border-radius: Formatting.rectangle-radius; // ----- ------mod-boxes need to have a max text length------------- // implmented a static way to elide text adding displayname property mod-box := CheckBox { @@ -98,7 +99,7 @@ export component MainPage inherits Page { accept } HorizontalLayout { - spacing: 7px; + spacing: Formatting.button-spacing; input-mod := LineEdit { height: 35px; preferred-width: 100px; diff --git a/ui/settings.slint b/ui/settings.slint index 7f5f708..32c2c2f 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -1,8 +1,7 @@ -import { GroupBox, Button, HorizontalBox, Switch, LineEdit } from "std-widgets.slint"; -import { MainLogic, SettingsLogic, Page } from "common.slint"; +import { GroupBox, Button, Switch, LineEdit } from "std-widgets.slint"; +import { MainLogic, SettingsLogic, Page, Formatting } from "common.slint"; export component SettingsPage inherits Page { - property horizontal-box-width: self.width - 15px; has-back-button: true; title: @tr("Settings"); description: @tr("Set path to eldenring.exe and app settings here"); @@ -10,20 +9,18 @@ export component SettingsPage inherits Page { VerticalLayout { y: 34px; height: parent.height - self.y; - padding-left: 8px; - padding-right: 0; - spacing: 0px; + padding-left: Formatting.side-spacing; alignment: space-between; GroupBox { title: @tr("General"); height: 70px; + width: Formatting.group-box-width; HorizontalLayout { - padding-top: 7px; - padding-left: 6px; - padding-right: 6px; - width: horizontal-box-width; + padding-top: Formatting.side-spacing - 4px; + padding-left: Formatting.side-spacing; + padding-right: Formatting.side-spacing; Switch { text: @tr("Dark Mode"); checked <=> SettingsLogic.dark-mode; @@ -44,12 +41,12 @@ export component SettingsPage inherits Page { GroupBox { title: @tr("Game Path"); height: 110px; + width: Formatting.group-box-width; - HorizontalBox { + HorizontalLayout { row: 1; - width: horizontal-box-width; - padding-top: 3px; - padding-bottom: 0; + padding-top: 2px; + padding-left: Formatting.side-spacing; Text { vertical-alignment: center; @@ -58,14 +55,14 @@ export component SettingsPage inherits Page { text: SettingsLogic.game-path; } } - HorizontalBox { + HorizontalLayout { row: 2; - padding-top: 9px; - padding-bottom: 0; - width: horizontal-box-width; + padding-top: Formatting.side-spacing + 1px; + padding-right: Formatting.side-spacing; + spacing: Formatting.button-spacing; alignment: end; Button { - width: 45px; + width: 42px; height: 30px; icon: @image-url("assets/folder.png"); colorize-icon: true; @@ -100,14 +97,12 @@ export component SettingsPage inherits Page { GroupBox { title: @tr("Mod Loader Options"); enabled: SettingsLogic.loader-installed; - height: 150px; + width: Formatting.group-box-width; + height: 140px; - HorizontalBox { + HorizontalLayout { row: 1; - padding-top: 4px; - padding-left: 2px; - padding-bottom: 5px; - width: horizontal-box-width; + padding-left: Formatting.side-spacing - 2px; Switch { text: @tr("Show Terminal"); enabled: SettingsLogic.loader-installed; @@ -115,13 +110,10 @@ export component SettingsPage inherits Page { toggled => { SettingsLogic.toggle-terminal(self.checked) } } } - HorizontalBox { + HorizontalLayout { row: 2; - padding-top: 4px; - padding-left: 2px; - padding-bottom: 5px; - - width: horizontal-box-width; + padding-top: Formatting.side-spacing; + padding-left: Formatting.side-spacing - 2px; Switch { text: @tr("Disable All mods"); enabled: SettingsLogic.loader-installed; @@ -129,13 +121,14 @@ export component SettingsPage inherits Page { toggled => { SettingsLogic.toggle-all(self.checked) } } } - HorizontalBox { + HorizontalLayout { row: 3; - padding-left: 2px; - padding-bottom: 8px; - width: horizontal-box-width; + padding-top: Formatting.side-spacing + 2px; + padding-right: Formatting.side-spacing; + padding-bottom: Formatting.side-spacing / 2; + spacing: Formatting.button-spacing; load-delay := LineEdit { - width: 128px; + width: 132px; height: 30px; horizontal-alignment: right; enabled: SettingsLogic.loader-installed; diff --git a/ui/tab-bar.slint b/ui/tab-bar.slint index 3db2a5d..315a21e 100644 --- a/ui/tab-bar.slint +++ b/ui/tab-bar.slint @@ -1,4 +1,4 @@ -import { ColorPalette, Formating } from "common.slint"; +import { ColorPalette, Formatting } from "common.slint"; component TabItem inherits Rectangle { in property selected; @@ -6,8 +6,8 @@ component TabItem inherits Rectangle { callback clicked <=> touch.clicked; - height: Formating.tab-bar-height; - width: Formating.app-width / 2; + height: Formatting.tab-bar-height; + width: Formatting.app-width / 2; states [ pressed when touch.pressed : { @@ -33,12 +33,12 @@ component TabItem inherits Rectangle { HorizontalLayout { y: (parent.height - self.height) / 2; height: parent.height; - padding: Formating.default-padding; + padding: Formatting.default-padding; padding-right: 8px; spacing: 0px; label := Text { - color: ColorPalette.text-base; + color: ColorPalette.text-foreground-color; font-size: 14px; vertical-alignment: center; horizontal-alignment: right; @@ -55,15 +55,15 @@ export component TabBar inherits Rectangle { in property <[string]> model: []; in-out property current-item: 0; - background: ColorPalette.page-background-color.darker(0.2); - border-top-left-radius: 10px; - border-top-right-radius: 10px; - width: Formating.app-width; - height: Formating.tab-bar-height; + background: ColorPalette.page-background-color.darker(0.16); + border-top-left-radius: Formatting.rectangle-radius; + border-top-right-radius: Formatting.rectangle-radius; + width: Formatting.app-width; + height: Formatting.tab-bar-height; HorizontalLayout { - padding-top: Formating.default-padding; - spacing: Formating.default-spacing; + padding-top: Formatting.default-padding; + spacing: Formatting.default-spacing; alignment: start; navigation := HorizontalLayout { diff --git a/ui/tabs.slint b/ui/tabs.slint index f587c65..bf71a29 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -1,11 +1,11 @@ import { ScrollView, GroupBox, Button } from "std-widgets.slint"; -import { Tab, SettingsLogic, MainLogic, Formating } from "common.slint"; +import { Tab, SettingsLogic, MainLogic, Formatting } from "common.slint"; export component ModDetails inherits Tab { in property mod-index; details := VerticalLayout { y: 0px; - padding-top: Formating.default-padding; + padding-top: Formatting.default-padding; padding-left: 12px; padding-right: 12px; alignment: start; @@ -16,8 +16,8 @@ export component ModDetails inherits Tab { } b := HorizontalLayout { padding-left: 8px; - padding-top: Formating.default-padding; - padding-bottom: Formating.default-padding; + padding-top: Formatting.default-padding; + padding-bottom: Formatting.default-padding; Text { font-size: 13pt; wrap: word-wrap; @@ -32,7 +32,7 @@ export component ModDetails inherits Tab { ScrollView { y: details.y + a.height + b.height + c.height + 9px; height: root.height - 75px; - width: Formating.layout-width; + width: Formatting.layout-width; viewport-height: files-txt.height; viewport-width: files-txt.width; files-txt := Text { @@ -47,23 +47,23 @@ export component ModDetails inherits Tab { export component ModEdit inherits Tab { in property mod-index; - property button-width: MainLogic.current-mods[mod-index].has-config ? 93px : 105px; - property button-layout: MainLogic.current-mods[mod-index].has-config ? center : end; + property button-width: MainLogic.current-mods[mod-index].has-config ? 96px : 105px; + property button-layout: MainLogic.current-mods[mod-index].has-config ? space-between : end; VerticalLayout { - y: root.height - edit-mod-box.height; - height: root.height - self.y; - padding-left: 8px; + y: 0px; + padding-left: Formatting.side-spacing; alignment: end; edit-mod-box := GroupBox { title: @tr("Edit Mod"); - height: 95px; + width: Formatting.group-box-width; + height: 90px; HorizontalLayout { - width: Formating.layout-width; - spacing: 7px; - padding-right: 8px; - padding-bottom: 8px; + width: Formatting.layout-width; + padding-right: Formatting.side-spacing; + padding-bottom: Formatting.side-spacing / 2; alignment: button-layout; + spacing: Formatting.button-spacing; Button { width: button-width; height: 35px; From aee3fbdd89298f2f40f53933e4dae477df46f9b5 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 21 Apr 2024 23:34:48 -0500 Subject: [PATCH 04/62] Combined formatting properties --- ui/common.slint | 14 ++++++++------ ui/editmod.slint | 4 ++-- ui/main.slint | 10 ++++------ ui/settings.slint | 26 +++++++++++++------------- ui/tab-bar.slint | 4 +--- ui/tabs.slint | 24 ++++++++++++------------ 6 files changed, 40 insertions(+), 42 deletions(-) diff --git a/ui/common.slint b/ui/common.slint index 9b87f25..02bb7d3 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -77,13 +77,15 @@ export global Formatting { out property app-preferred-height: 381px; out property header-height: 48px; out property tab-bar-height: 30px; - out property layout-width: app-width - 10px; out property default-padding: 3px; out property default-spacing: 3px; - out property side-spacing: 8px; + out property side-padding: 8px; out property button-spacing: 5px; out property rectangle-radius: 10px; - out property group-box-width: app-width - Formatting.side-spacing; + out property group-box-width: app-width - Formatting.side-padding; + out property font-size-h1: 18pt; + out property font-size-h2: 14pt; + out property font-size-h3: 10pt; } export component Page inherits Rectangle { @@ -114,7 +116,7 @@ export component Page inherits Rectangle { source: @image-url("assets/back-arrow.png"); image-fit: contain; colorize: ColorPalette.text-foreground-color; - source-clip-y: -50; + source-clip-y: - 50; width: 30px; height: 24px; @@ -126,7 +128,7 @@ export component Page inherits Rectangle { } } title := Text { - font-size: 24px; + font-size: Formatting.font-size-h1; max-width: root.width - 10px; text: root.title; color: ColorPalette.text-foreground-color; @@ -149,7 +151,7 @@ export component Page inherits Rectangle { if (!root.has-back-button) : HorizontalLayout { Text { - font-size: 24px; + font-size: Formatting.font-size-h1; max-width: root.width * 0.8; text: root.title; color: ColorPalette.text-foreground-color; diff --git a/ui/editmod.slint b/ui/editmod.slint index 781bbdb..b997d0d 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -24,9 +24,9 @@ export component ModDetailsPage inherits Page { y: Formatting.header-height - header-offset; width: Formatting.app-width; height: 27px; - padding-right: 8px; + padding-right: Formatting.side-padding; Text { - font-size: 16pt; + font-size: Formatting.font-size-h2; color: state-color; text: state; horizontal-alignment: right; diff --git a/ui/main.slint b/ui/main.slint index c1952c2..ede6702 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -23,8 +23,8 @@ export component MainPage inherits Page { y: 27px; height: parent.height - self.y; preferred-width: Formatting.app-width; - padding: Formatting.side-spacing; - padding-bottom: Formatting.side-spacing / 2; + padding: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; reg-mod-box := GroupBox { title: @tr("Registered-Mods:"); @@ -80,7 +80,7 @@ export component MainPage inherits Page { } } add-mod-box := GroupBox { - height: 88px; + height: 85px; title: @tr("Add Mod"); enabled: MainLogic.game-path-valid; FocusScope { @@ -102,15 +102,13 @@ export component MainPage inherits Page { spacing: Formatting.button-spacing; input-mod := LineEdit { height: 35px; - preferred-width: 100px; - horizontal-alignment: left; placeholder-text: @tr("Mod Name"); enabled: add-mod-box.enabled; text <=> MainLogic.line-edit-text; } add-mod := Button { - width: 89px; height: 35px; + width: 95px; text: @tr("Select Files"); primary: !SettingsLogic.dark-mode; enabled: add-mod-box.enabled; diff --git a/ui/settings.slint b/ui/settings.slint index 32c2c2f..3cf0c91 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -9,7 +9,7 @@ export component SettingsPage inherits Page { VerticalLayout { y: 34px; height: parent.height - self.y; - padding-left: Formatting.side-spacing; + padding-left: Formatting.side-padding; alignment: space-between; GroupBox { @@ -18,9 +18,9 @@ export component SettingsPage inherits Page { width: Formatting.group-box-width; HorizontalLayout { - padding-top: Formatting.side-spacing - 4px; - padding-left: Formatting.side-spacing; - padding-right: Formatting.side-spacing; + padding-top: Formatting.side-padding / 2; + padding-left: Formatting.side-padding; + padding-right: Formatting.side-padding; Switch { text: @tr("Dark Mode"); checked <=> SettingsLogic.dark-mode; @@ -46,7 +46,7 @@ export component SettingsPage inherits Page { HorizontalLayout { row: 1; padding-top: 2px; - padding-left: Formatting.side-spacing; + padding-left: Formatting.side-padding; Text { vertical-alignment: center; @@ -57,8 +57,8 @@ export component SettingsPage inherits Page { } HorizontalLayout { row: 2; - padding-top: Formatting.side-spacing + 1px; - padding-right: Formatting.side-spacing; + padding-top: Formatting.side-padding + 1px; + padding-right: Formatting.side-padding; spacing: Formatting.button-spacing; alignment: end; Button { @@ -102,7 +102,7 @@ export component SettingsPage inherits Page { HorizontalLayout { row: 1; - padding-left: Formatting.side-spacing - 2px; + padding-left: Formatting.side-padding - 2px; Switch { text: @tr("Show Terminal"); enabled: SettingsLogic.loader-installed; @@ -112,8 +112,8 @@ export component SettingsPage inherits Page { } HorizontalLayout { row: 2; - padding-top: Formatting.side-spacing; - padding-left: Formatting.side-spacing - 2px; + padding-top: Formatting.side-padding; + padding-left: Formatting.side-padding - 2px; Switch { text: @tr("Disable All mods"); enabled: SettingsLogic.loader-installed; @@ -123,9 +123,9 @@ export component SettingsPage inherits Page { } HorizontalLayout { row: 3; - padding-top: Formatting.side-spacing + 2px; - padding-right: Formatting.side-spacing; - padding-bottom: Formatting.side-spacing / 2; + padding-top: Formatting.side-padding + 2px; + padding-right: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; spacing: Formatting.button-spacing; load-delay := LineEdit { width: 132px; diff --git a/ui/tab-bar.slint b/ui/tab-bar.slint index 315a21e..f95e2af 100644 --- a/ui/tab-bar.slint +++ b/ui/tab-bar.slint @@ -32,14 +32,12 @@ component TabItem inherits Rectangle { HorizontalLayout { y: (parent.height - self.height) / 2; - height: parent.height; padding: Formatting.default-padding; padding-right: 8px; - spacing: 0px; label := Text { color: ColorPalette.text-foreground-color; - font-size: 14px; + font-size: Formatting.font-size-h3; vertical-alignment: center; horizontal-alignment: right; } diff --git a/ui/tabs.slint b/ui/tabs.slint index bf71a29..c63f6dd 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -6,39 +6,39 @@ export component ModDetails inherits Tab { details := VerticalLayout { y: 0px; padding-top: Formatting.default-padding; - padding-left: 12px; - padding-right: 12px; + padding-left: Formatting.side-padding; + padding-right: Formatting.side-padding; alignment: start; a := Text { - font-size: 10pt; + font-size: Formatting.font-size-h3; text: @tr("Name:"); } b := HorizontalLayout { - padding-left: 8px; + padding-left: Formatting.side-padding; padding-top: Formatting.default-padding; padding-bottom: Formatting.default-padding; Text { - font-size: 13pt; + font-size: Formatting.font-size-h2; wrap: word-wrap; text: MainLogic.current-mods[mod-index].name; } } c := Text { - font-size: 10pt; + font-size: Formatting.font-size-h3; text: @tr("Files:"); } } ScrollView { y: details.y + a.height + b.height + c.height + 9px; height: root.height - 75px; - width: Formatting.layout-width; + width: Formatting.group-box-width; viewport-height: files-txt.height; viewport-width: files-txt.width; files-txt := Text { x: 15px; width: parent.width - self.x; - font-size: 13pt; + font-size: Formatting.font-size-h2; wrap: word-wrap; text: MainLogic.current-mods[mod-index].files; } @@ -51,7 +51,7 @@ export component ModEdit inherits Tab { property button-layout: MainLogic.current-mods[mod-index].has-config ? space-between : end; VerticalLayout { y: 0px; - padding-left: Formatting.side-spacing; + padding-left: Formatting.side-padding; alignment: end; edit-mod-box := GroupBox { @@ -59,9 +59,9 @@ export component ModEdit inherits Tab { width: Formatting.group-box-width; height: 90px; HorizontalLayout { - width: Formatting.layout-width; - padding-right: Formatting.side-spacing; - padding-bottom: Formatting.side-spacing / 2; + width: Formatting.group-box-width; + padding-right: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; alignment: button-layout; spacing: Formatting.button-spacing; Button { From 92fd83ba5a930a626930066d397938b8880ef4fc Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 22 Apr 2024 00:55:15 -0500 Subject: [PATCH 05/62] Updated focus scope controls fixed focus scope bug on key event press "tab" was able to access scope outside of curent sub-page now able to use the tab key to select the line edit on the settings page as well. --- ui/appwindow.slint | 6 +++++- ui/main.slint | 4 +++- ui/settings.slint | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/appwindow.slint b/ui/appwindow.slint index 796d306..b5a0256 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -189,7 +189,11 @@ export component App inherits Window { } if (event.text == Key.Tab) { if (!popup-visible) { - mp.focus-line-edit() + if (MainLogic.current-subpage == 0) { + mp.focus-line-edit() + } else if (MainLogic.current-subpage == 1) { + mp.focus-settings() + } } } accept diff --git a/ui/main.slint b/ui/main.slint index ede6702..7ac9ae0 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -11,8 +11,10 @@ export component MainPage inherits Page { // height: 400px; callback focus-line-edit; + callback focus-settings; callback edit-mod(int); focus-line-edit => { input-mod.focus() } + focus-settings => { app-settings.focus-settings-scope() } edit-mod(i) => { mod-settings.current-tab = 0; mod-settings.mod-index = i; @@ -125,7 +127,7 @@ export component MainPage inherits Page { } } } - SettingsPage { + app-settings := SettingsPage { x: MainLogic.current-subpage == 1 ? 0 : parent.width + parent.x + 2px; animate x { duration: 150ms; easing: ease; } } diff --git a/ui/settings.slint b/ui/settings.slint index 3cf0c91..9abf9f9 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -6,6 +6,9 @@ export component SettingsPage inherits Page { title: @tr("Settings"); description: @tr("Set path to eldenring.exe and app settings here"); + callback focus-settings-scope; + focus-settings-scope => { load-delay.focus() } + VerticalLayout { y: 34px; height: parent.height - self.y; From 5305c257dcdb86708571a52dcb6c188be7b36641 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 22 Apr 2024 00:57:18 -0500 Subject: [PATCH 06/62] Code cleanup used if let for enum pattern matching, for better readability and now we don't have to derive PartialEq on our Enum Removed unneeded variable binding --- src/utils/installer.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/utils/installer.rs b/src/utils/installer.rs index fab9b41..19c9813 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -132,7 +132,6 @@ fn parent_dir_from_vec(in_files: &[PathBuf]) -> std::io::Result { } } -#[derive(PartialEq)] pub enum DisplayItems { Limit(usize), All, @@ -378,7 +377,7 @@ impl InstallData { format_loop(self, &mut files_to_display, directory, &mut cut_off_data)?; - if *cutoff != DisplayItems::None { + if let DisplayItems::All | DisplayItems::Limit(_) = *cutoff { self.display_paths = files_to_display.join("\n"); } @@ -439,9 +438,8 @@ impl InstallData { }); match jh.join() { Ok(result) => match result { - Ok(data) => { - let mut new_self = data; - std::mem::swap(&mut new_self, self); + Ok(mut data) => { + std::mem::swap(&mut data, self); Ok(()) } Err(err) => Err(err), @@ -530,6 +528,9 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result for file in files.iter() { let name = file_name_or_err(file)?.to_string_lossy(); let file_data = FileData::from(&name); + if file_data.extension != ".dll" { + continue; + }; if let Some(dir) = dirs .iter() .find(|d| d.file_name().expect("is dir") == file_data.name) From d4789607224e8ed1e1906221214507c06fa458ec Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 22 Apr 2024 11:14:07 -0500 Subject: [PATCH 07/62] Added focus scope feature Added feature for using tab to swap tabs. Added feature for escape to work properly when line edit is focused on the settings page. Fixed enabled state bug with the focus scope --- ui/appwindow.slint | 11 +++++++++-- ui/common.slint | 2 +- ui/main.slint | 16 +++++++--------- ui/settings.slint | 9 ++------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ui/appwindow.slint b/ui/appwindow.slint index b5a0256..2e08cd5 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -187,12 +187,19 @@ export component App inherits Window { MainLogic.current-subpage = 0 } } + // && doesn't work in slint conditonal statements if (event.text == Key.Tab) { if (!popup-visible) { if (MainLogic.current-subpage == 0) { - mp.focus-line-edit() + if (MainLogic.game-path-valid) { + mp.focus-line-edit() + } } else if (MainLogic.current-subpage == 1) { - mp.focus-settings() + if (SettingsLogic.loader-installed) { + mp.focus-settings() + } + } else if (MainLogic.current-subpage == 2) { + mp.swap-tab() } } } diff --git a/ui/common.slint b/ui/common.slint index 02bb7d3..3b11fe7 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -98,7 +98,7 @@ export component Page inherits Rectangle { callback back; callback settings; - back => { MainLogic.current-subpage = 0 } + back => { MainLogic.current-subpage = 0; MainLogic.force-app-focus() } settings => { MainLogic.current-subpage = 1 } TouchArea {} // Protect underneath controls diff --git a/ui/main.slint b/ui/main.slint index 7ac9ae0..5ddf494 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -12,9 +12,11 @@ export component MainPage inherits Page { callback focus-line-edit; callback focus-settings; + callback swap-tab; callback edit-mod(int); focus-line-edit => { input-mod.focus() } focus-settings => { app-settings.focus-settings-scope() } + swap-tab => { mod-settings.current-tab = mod-settings.current-tab == 0 ? 1 : 0 } edit-mod(i) => { mod-settings.current-tab = 0; mod-settings.mod-index = i; @@ -81,22 +83,18 @@ export component MainPage inherits Page { } } } - add-mod-box := GroupBox { + GroupBox { height: 85px; title: @tr("Add Mod"); enabled: MainLogic.game-path-valid; FocusScope { + enabled: parent.enabled; key-pressed(event) => { if (event.text == Key.Escape) { MainLogic.force-app-focus() } - // && doesn't work in slint conditonal statements if (event.text == Key.Tab) { - if (input-mod.has-focus) { - add-mod.focus() - } else { - input-mod.focus() - } + input-mod.has-focus ? add-mod.focus() : input-mod.focus() } accept } @@ -105,7 +103,7 @@ export component MainPage inherits Page { input-mod := LineEdit { height: 35px; placeholder-text: @tr("Mod Name"); - enabled: add-mod-box.enabled; + enabled: MainLogic.game-path-valid; text <=> MainLogic.line-edit-text; } add-mod := Button { @@ -113,7 +111,7 @@ export component MainPage inherits Page { width: 95px; text: @tr("Select Files"); primary: !SettingsLogic.dark-mode; - enabled: add-mod-box.enabled; + enabled: MainLogic.game-path-valid; clicked => { if(input-mod.text != "") { MainLogic.force-app-focus(); diff --git a/ui/settings.slint b/ui/settings.slint index 9abf9f9..9a9d065 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -84,15 +84,10 @@ export component SettingsPage inherits Page { FocusScope { key-pressed(event) => { if (event.text == Key.Escape) { - MainLogic.force-app-focus() + root.back() } - // && doesn't work in slint conditonal statements if (event.text == Key.Tab) { - if (load-delay.has-focus) { - set-delay.focus() - } else { - load-delay.focus() - } + load-delay.has-focus ? set-delay.focus() : load-delay.focus() } accept } From 718a17312c1cba51ba59497414f548bb9b482831 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 14:39:10 -0500 Subject: [PATCH 08/62] UI changes and code cleanup Moved the tab bar into common.slint Cleaned up lots of un-needed UI elements File list now uses a StandardListView widget --- ui/appwindow.slint | 6 +--- ui/common.slint | 79 +++++++++++++++++++++++++++++++++++++++++++--- ui/editmod.slint | 23 ++++---------- ui/tab-bar.slint | 77 -------------------------------------------- ui/tabs.slint | 33 +++++++++---------- 5 files changed, 98 insertions(+), 120 deletions(-) delete mode 100644 ui/tab-bar.slint diff --git a/ui/appwindow.slint b/ui/appwindow.slint index 2e08cd5..15d1227 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -50,11 +50,7 @@ export component App inherits Window { } show-confirm-popup => { popup-visible = true; - if (!alt-std-buttons) { - confirm-popup.show() - } else { - confirm-popup-2.show() - } + alt-std-buttons ? confirm-popup-2.show() : confirm-popup.show() } msg-size := Text { diff --git a/ui/common.slint b/ui/common.slint index 3b11fe7..69d25f4 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -2,7 +2,7 @@ export struct DisplayMod { displayname: string, name: string, enabled: bool, - files: string, + files: [StandardListViewItem], has-config: bool, config-files: [string], } @@ -15,6 +15,7 @@ export global MainLogic { callback add-to-mod(string); callback remove-mod(string); callback edit-config([string]); + callback edit-config-item(StandardListViewItem); callback force-app-focus(); callback send-message(Message); in property line-edit-text; @@ -22,7 +23,7 @@ export global MainLogic { in-out property game-path-valid; in-out property current-subpage: 0; in-out property <[DisplayMod]> current-mods: [ - {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, files: "\\placeholder\\path\\data\\really\\long\\paths"}, + {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true}, ]; } @@ -50,6 +51,7 @@ struct ButtonColors { export global ColorPalette { out property page-background-color: SettingsLogic.dark-mode ? #1b1b1b : #60a0a4; + out property alt-page-background-color: SettingsLogic.dark-mode ? #132b4e : #747e81; out property popup-background-color: SettingsLogic.dark-mode ? #00393d : #1b1b1b; out property popup-border-color: SettingsLogic.dark-mode ? #17575c : #1b1b1b; @@ -92,9 +94,9 @@ export component Page inherits Rectangle { in property title: "title"; in property description: "description"; in property has-back-button; - in property dark-header; + in property alt-background; width: Formatting.app-width; - background: dark-header ? ColorPalette.page-background-color.darker(0.16) : ColorPalette.page-background-color; + background: alt-background ? ColorPalette.alt-page-background-color : ColorPalette.page-background-color; callback back; callback settings; @@ -198,4 +200,73 @@ export component Tab inherits Rectangle { TouchArea {} // Protect underneath controls @children +} + +component TabItem inherits Rectangle { + in property selected; + in-out property text <=> label.text; + + callback clicked <=> touch.clicked; + + height: Formatting.tab-bar-height; + + states [ + pressed when touch.pressed : { + state.opacity: 0.9; + } + hover when touch.has-hover : { + state.opacity: 1; + } + un-selected when !root.selected : { + state.opacity: 0.80; + } + selected when root.selected : { + state.opacity: 1; + } + ] + + state := Rectangle { + background: ColorPalette.page-background-color; + border-top-left-radius: 13px; + border-top-right-radius: 13px; + + animate opacity { duration: 150ms; } + } + + HorizontalLayout { + y: (parent.height - self.height) / 2; + padding: Formatting.default-padding; + padding-right: Formatting.side-padding; + + label := Text { + color: ColorPalette.text-foreground-color; + font-size: Formatting.font-size-h3; + vertical-alignment: center; + horizontal-alignment: right; + } + } + + touch := TouchArea { + width: 100%; + height: 100%; + } +} + +export component TabBar inherits Rectangle { + in property <[string]> model: []; + in-out property current-item: 0; + + width: Formatting.app-width; + height: Formatting.tab-bar-height; + + HorizontalLayout { + alignment: start; + horizontal-stretch: 0; + for item[index] in root.model : TabItem { + clicked => { root.current-item = index; } + text: item; + width: root.width / root.model.length; + selected: index == root.current-item; + } + } } \ No newline at end of file diff --git a/ui/editmod.slint b/ui/editmod.slint index b997d0d..b17404b 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -1,10 +1,8 @@ -import { GroupBox, Button, ScrollView } from "std-widgets.slint"; -import { MainLogic, SettingsLogic, Page, Tab, Formatting, ColorPalette } from "common.slint"; -import { TabBar } from "tab-bar.slint"; +import { MainLogic, SettingsLogic, Page, Formatting, TabBar} from "common.slint"; import { ModDetails, ModEdit } from "tabs.slint"; export component ModDetailsPage inherits Page { - dark-header: true; + alt-background: true; has-back-button: true; title: MainLogic.current-mods[mod-index].name; description: @tr("Edit registered mods here"); @@ -18,7 +16,9 @@ export component ModDetailsPage inherits Page { MainLogic.current-mods[mod-index].enabled ? #206816 : #d01616; property state: SettingsLogic.loader-disabled ? @tr("Mod Loader Disabled") : MainLogic.current-mods[mod-index].enabled ? @tr("Mod Enabled") : @tr("Mod Disabled"); - property header-offset: 9px; + property header-offset: 12px; + property tab-height: self.height - Formatting.header-height - info-text.height - tab-bar.height + header-offset; + property tab-y: Formatting.header-height + tab-bar.height + info-text.height - header-offset; info-text := HorizontalLayout { y: Formatting.header-height - header-offset; @@ -31,7 +31,6 @@ export component ModDetailsPage inherits Page { text: state; horizontal-alignment: right; } - } tab-bar := TabBar { @@ -40,15 +39,7 @@ export component ModDetailsPage inherits Page { current-item <=> current-tab; } - if(tab-bar.current-item == 0) : ModDetails { - mod-index: mod-index; - height: root.height - Formatting.header-height - info-text.height - tab-bar.height + header-offset; - y: Formatting.header-height + tab-bar.height + info-text.height - header-offset; - } - if(tab-bar.current-item == 1) : ModEdit { - mod-index: mod-index; - height: root.height - Formatting.header-height - info-text.height - tab-bar.height + header-offset; - y: Formatting.header-height + tab-bar.height + info-text.height - header-offset; - } + if tab-bar.current-item == 0 : ModDetails { mod-index: mod-index; height: tab-height; y: tab-y; } + if tab-bar.current-item == 1 : ModEdit { mod-index: mod-index; height: tab-height; y: tab-y; } } \ No newline at end of file diff --git a/ui/tab-bar.slint b/ui/tab-bar.slint deleted file mode 100644 index f95e2af..0000000 --- a/ui/tab-bar.slint +++ /dev/null @@ -1,77 +0,0 @@ -import { ColorPalette, Formatting } from "common.slint"; - -component TabItem inherits Rectangle { - in property selected; - in-out property text <=> label.text; - - callback clicked <=> touch.clicked; - - height: Formatting.tab-bar-height; - width: Formatting.app-width / 2; - - states [ - pressed when touch.pressed : { - state.opacity: 0.8; - } - hover when touch.has-hover : { - state.opacity: 0.6; - } - selected when root.selected : { - state.opacity: 1; - } - ] - - state := Rectangle { - opacity: 0; - background: ColorPalette.page-background-color; - border-top-left-radius: 13px; - border-top-right-radius: 13px; - - animate opacity { duration: 150ms; } - } - - HorizontalLayout { - y: (parent.height - self.height) / 2; - padding: Formatting.default-padding; - padding-right: 8px; - - label := Text { - color: ColorPalette.text-foreground-color; - font-size: Formatting.font-size-h3; - vertical-alignment: center; - horizontal-alignment: right; - } - } - - touch := TouchArea { - width: 100%; - height: 100%; - } -} - -export component TabBar inherits Rectangle { - in property <[string]> model: []; - in-out property current-item: 0; - - background: ColorPalette.page-background-color.darker(0.16); - border-top-left-radius: Formatting.rectangle-radius; - border-top-right-radius: Formatting.rectangle-radius; - width: Formatting.app-width; - height: Formatting.tab-bar-height; - - HorizontalLayout { - padding-top: Formatting.default-padding; - spacing: Formatting.default-spacing; - alignment: start; - - navigation := HorizontalLayout { - alignment: start; - vertical-stretch: 0; - for item[index] in root.model : TabItem { - clicked => { root.current-item = index; } - text: item; - selected: index == root.current-item; - } - } - } -} \ No newline at end of file diff --git a/ui/tabs.slint b/ui/tabs.slint index c63f6dd..52f5ce6 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -1,13 +1,15 @@ -import { ScrollView, GroupBox, Button } from "std-widgets.slint"; +import { GroupBox, Button, StandardListView } from "std-widgets.slint"; import { Tab, SettingsLogic, MainLogic, Formatting } from "common.slint"; export component ModDetails inherits Tab { in property mod-index; - details := VerticalLayout { + property details-height: a.height + b.height + c.height + (3*Formatting.default-spacing); + VerticalLayout { y: 0px; padding-top: Formatting.default-padding; - padding-left: Formatting.side-padding; - padding-right: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; + padding: Formatting.side-padding; + spacing: Formatting.default-spacing; alignment: start; a := Text { @@ -16,8 +18,6 @@ export component ModDetails inherits Tab { } b := HorizontalLayout { padding-left: Formatting.side-padding; - padding-top: Formatting.default-padding; - padding-bottom: Formatting.default-padding; Text { font-size: Formatting.font-size-h2; wrap: word-wrap; @@ -29,18 +29,15 @@ export component ModDetails inherits Tab { text: @tr("Files:"); } } - ScrollView { - y: details.y + a.height + b.height + c.height + 9px; - height: root.height - 75px; - width: Formatting.group-box-width; - viewport-height: files-txt.height; - viewport-width: files-txt.width; - files-txt := Text { - x: 15px; - width: parent.width - self.x; - font-size: Formatting.font-size-h2; - wrap: word-wrap; - text: MainLogic.current-mods[mod-index].files; + StandardListView { + y: details-height; + height: root.height - details-height - Formatting.side-padding; + width: Formatting.group-box-width - Formatting.side-padding; + model: MainLogic.current-mods[mod-index].files; + item-pointer-event(i, event) => { + if event.kind == PointerEventKind.up { + MainLogic.edit-config-item(MainLogic.current-mods[mod-index].files[i]) + } } } } From d3f21cff195d6c345955b8ff4f3950331e65e686 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 14:41:31 -0500 Subject: [PATCH 09/62] Feat: Open files from details list Selecting a file in the details tab of a mod will now open up in notepad! This works for all .txt and .ini files --- src/main.rs | 81 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5308661..bf5decb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -677,6 +677,19 @@ fn main() -> Result<(), slint::PlatformError> { ); } }); + ui.global::().on_edit_config_item({ + let ui_handle = ui.as_weak(); + move |config_item| { + let ui = ui_handle.unwrap(); + let game_dir = ui.global::().get_game_path(); + let item = config_item.text.to_string(); + if !matches!(FileData::from(&item).extension, ".txt" | ".ini") { + return; + }; + let os_file = vec![std::ffi::OsString::from(format!("{game_dir}\\{item}"))]; + open_text_files(ui.as_weak(), os_file); + } + }); ui.global::().on_edit_config({ let ui_handle = ui.as_weak(); move |config_file| { @@ -686,33 +699,11 @@ fn main() -> Result<(), slint::PlatformError> { .as_any() .downcast_ref::>() .expect("We know we set a VecModel earlier"); - let string_file = downcast_config_file + let os_files = downcast_config_file .iter() .map(|path| std::ffi::OsString::from(format!("{game_dir}\\{path}"))) .collect::>(); - for file in string_file { - let file_clone = file.clone(); - let jh = std::thread::spawn(move || { - std::process::Command::new("notepad") - .arg(&file) - .spawn() - }); - match jh.join() { - Ok(result) => match result { - Ok(_) => (), - Err(err) => { - error!("{err}"); - ui.display_msg(&format!( - "Failed to open config file {file_clone:?}\n\nError: {err}" - )); - } - }, - Err(err) => { - error!("Thread panicked! {err:?}"); - ui.display_msg(&format!("{err:?}")); - } - } - } + open_text_files(ui.as_weak(), os_files); } }); ui.global::().on_toggle_terminal({ @@ -914,11 +905,42 @@ fn populate_restricted_files() -> [&'static OsStr; 6] { restricted_files } +fn open_text_files(ui_handle: slint::Weak, files: Vec) { + let ui = ui_handle.unwrap(); + for file in files { + let file_clone = file.clone(); + let jh = std::thread::spawn(move || { + std::process::Command::new("notepad") + .arg(&file) + .spawn() + }); + match jh.join() { + Ok(result) => match result { + Ok(_) => (), + Err(err) => { + error!("{err}"); + ui.display_msg(&format!( + "Failed to open config file {file_clone:?}\n\nError: {err}" + )); + } + }, + Err(err) => { + error!("Thread panicked! {err:?}"); + ui.display_msg(&format!("{err:?}")); + } + } + } +} + fn deserialize(data: &[RegMod]) -> ModelRc { let display_mod: Rc> = Default::default(); for mod_data in data.iter() { let has_config = !mod_data.config_files.is_empty(); let config_files: Rc> = Default::default(); + let files: Rc> = Default::default(); + files.extend(mod_data.files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(".disabled", "")).into())); + files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); + files.extend(mod_data.other_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); if has_config { mod_data.config_files.iter().for_each(|file| { config_files.push(SharedString::from(file.to_string_lossy().to_string())) @@ -935,16 +957,7 @@ fn deserialize(data: &[RegMod]) -> ModelRc { }), name: SharedString::from(name.clone()), enabled: mod_data.state, - files: SharedString::from({ - let files: Vec = { - let mut files = Vec::with_capacity(mod_data.all_files_len()); - files.extend(mod_data.files.iter().map(|f| f.to_string_lossy().replace(".disabled", ""))); - files.extend(mod_data.config_files.iter().map(|f| f.to_string_lossy().to_string())); - files.extend(mod_data.other_files.iter().map(|f| f.to_string_lossy().to_string())); - files - }; - files.join("\n") - }), + files: ModelRc::from(files), has_config, config_files: ModelRc::from(config_files), }) From 65ad431a36eb09e5f8f54fd2f9eb01b192e225c6 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 15:48:18 -0500 Subject: [PATCH 10/62] RegMod.files is now mod_files This now makes it more clear dll files will be seprate, other_files is now more clear as well. This will prep us for being able to only select .dll files in the front end for setting load_order has_config_files bool is now calculated in front end as well. --- src/lib.rs | 4 ++-- src/main.rs | 40 ++++++++++++++++++++-------------------- src/utils/ini/parser.rs | 26 ++++++++++++-------------- src/utils/installer.rs | 4 ++-- tests/test_ini_tools.rs | 4 ++-- tests/test_lib.rs | 8 ++++---- ui/common.slint | 2 +- ui/tabs.slint | 7 ++++--- 8 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4fe1c7c..be845c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,10 +190,10 @@ pub fn toggle_files( save_bool(save_file, Some("registered-mods"), key, state)?; Ok(()) } - let num_rename_files = reg_mod.files.len(); + let num_rename_files = reg_mod.mod_files.len(); let num_total_files = num_rename_files + reg_mod.other_files_len(); - let file_paths = std::sync::Arc::new(reg_mod.files.clone()); + let file_paths = std::sync::Arc::new(reg_mod.mod_files.clone()); let file_paths_clone = file_paths.clone(); let game_dir_clone = game_dir.to_path_buf(); diff --git a/src/main.rs b/src/main.rs index bf5decb..4c89ced 100644 --- a/src/main.rs +++ b/src/main.rs @@ -486,7 +486,6 @@ fn main() -> Result<(), slint::PlatformError> { return; } }; - // let reciever_clone = reciever_clone.clone(); slint::spawn_local(async move { let file_paths = match get_user_files(&game_dir) { Ok(paths) => paths, @@ -536,11 +535,11 @@ fn main() -> Result<(), slint::PlatformError> { if file_registered(®istered_mods, &files) { ui.display_msg("A selected file is already registered to a mod"); } else { - let mut new_data = found_mod.files.clone(); + let mut new_data = found_mod.mod_files.clone(); new_data.extend(files); let mut results = Vec::with_capacity(2); let new_data_refs = found_mod.add_other_files_to_files(&new_data); - if found_mod.files.len() + found_mod.other_files_len() == 1 { + if found_mod.all_files_len() == 1 { results.push(remove_entry( current_ini, Some("mod-files"), @@ -620,7 +619,7 @@ fn main() -> Result<(), slint::PlatformError> { if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { - let mut found_files = found_mod.files.clone(); + let mut found_files = found_mod.mod_files.clone(); if found_files.iter().any(|file| { matches!(FileData::is_enabled(file), Ok(false)) }) { @@ -933,36 +932,37 @@ fn open_text_files(ui_handle: slint::Weak, files: Vec) } fn deserialize(data: &[RegMod]) -> ModelRc { - let display_mod: Rc> = Default::default(); + let display_mods: Rc> = Default::default(); for mod_data in data.iter() { - let has_config = !mod_data.config_files.is_empty(); - let config_files: Rc> = Default::default(); let files: Rc> = Default::default(); - files.extend(mod_data.files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(".disabled", "")).into())); - files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); - files.extend(mod_data.other_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); - if has_config { - mod_data.config_files.iter().for_each(|file| { - config_files.push(SharedString::from(file.to_string_lossy().to_string())) - }) - } else { - config_files.push(SharedString::new()) + let dll_files: Rc> = Default::default(); + let config_files: Rc> = Default::default(); + if !mod_data.mod_files.is_empty() { + files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(".disabled", "")).into())); + dll_files.extend(mod_data.mod_files.iter().map(|file|SharedString::from(file.to_string_lossy().to_string()))); + }; + if !mod_data.config_files.is_empty() { + files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); + config_files.extend(mod_data.config_files.iter().map(|file| SharedString::from(file.to_string_lossy().to_string()))); + }; + if !mod_data.other_files.is_empty() { + files.extend(mod_data.other_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); }; let name = mod_data.name.replace('_', " "); - display_mod.push(DisplayMod { + display_mods.push(DisplayMod { displayname: SharedString::from(if mod_data.name.chars().count() > 20 { format!("{}...", &name[..17]) } else { name.clone() }), - name: SharedString::from(name.clone()), + name: SharedString::from(name), enabled: mod_data.state, files: ModelRc::from(files), - has_config, config_files: ModelRc::from(config_files), + dll_files: ModelRc::from(dll_files) }) } - ModelRc::from(display_mod) + ModelRc::from(display_mods) } async fn install_new_mod( diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 21392f1..73b1355 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -260,7 +260,7 @@ impl Valitidity for Ini { pub struct RegMod { pub name: String, pub state: bool, - pub files: Vec, + pub mod_files: Vec, pub config_files: Vec, pub other_files: Vec, } @@ -270,25 +270,23 @@ impl RegMod { fn split_out_config_files( in_files: Vec, ) -> (Vec, Vec, Vec) { - let mut files = Vec::with_capacity(in_files.len()); + let mut mod_files = Vec::with_capacity(in_files.len()); let mut config_files = Vec::with_capacity(in_files.len()); let mut other_files = Vec::with_capacity(in_files.len()); in_files.into_iter().for_each(|file| { - let file_name = file.file_name().expect("is file").to_string_lossy(); - let name_data = FileData::from(&file_name); - match name_data.extension { - ".txt" => other_files.push(file), + match FileData::from(&file.to_string_lossy()).extension { + ".dll" => mod_files.push(file), ".ini" => config_files.push(file), - _ => files.push(file), + _ => other_files.push(file), } }); - (files, config_files, other_files) + (mod_files, config_files, other_files) } - let (files, config_files, other_files) = split_out_config_files(in_files); + let (mod_files, config_files, other_files) = split_out_config_files(in_files); RegMod { name: String::from(name), state, - files, + mod_files, config_files, other_files, } @@ -448,12 +446,12 @@ impl RegMod { pub fn verify_state(&self, game_dir: &Path, ini_file: &Path) -> std::io::Result<()> { if (!self.state && self - .files + .mod_files .iter() .any(|path| matches!(FileData::is_enabled(path), Ok(true)))) || (self.state && self - .files + .mod_files .iter() .any(|path| matches!(FileData::is_enabled(path), Ok(false)))) { @@ -467,7 +465,7 @@ impl RegMod { } pub fn file_refs(&self) -> Vec<&Path> { let mut path_refs = Vec::with_capacity(self.all_files_len()); - path_refs.extend(self.files.iter().map(|f| f.as_path())); + path_refs.extend(self.mod_files.iter().map(|f| f.as_path())); path_refs.extend(self.config_files.iter().map(|f| f.as_path())); path_refs.extend(self.other_files.iter().map(|f| f.as_path())); path_refs @@ -480,7 +478,7 @@ impl RegMod { path_refs } pub fn all_files_len(&self) -> usize { - self.files.len() + self.config_files.len() + self.other_files.len() + self.mod_files.len() + self.config_files.len() + self.other_files.len() } pub fn other_files_len(&self) -> usize { self.config_files.len() + self.other_files.len() diff --git a/src/utils/installer.rs b/src/utils/installer.rs index 19c9813..ee6fe11 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -216,8 +216,8 @@ impl InstallData { file_paths: Vec, game_dir: &Path, ) -> std::io::Result { - let amend_mod_split_file_names = amend_to.files.iter().try_fold( - Vec::with_capacity(amend_to.files.len()), + let amend_mod_split_file_names = amend_to.mod_files.iter().try_fold( + Vec::with_capacity(amend_to.mod_files.len()), |mut acc, file| { let file_name = file_name_or_err(file)?.to_string_lossy(); let file_data = FileData::from(&file_name); diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 0b8c7cc..501947c 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -135,9 +135,9 @@ mod tests { .unwrap(); // Tests if PathBuf and Vec's from Section("mod-files") parse correctly | these are partial paths - assert_eq!(mod_1[0], reg_mod_1.files[0]); + assert_eq!(mod_1[0], reg_mod_1.mod_files[0]); assert_eq!(mod_1[1], reg_mod_1.config_files[0]); - assert_eq!(mod_2, reg_mod_2.files[0]); + assert_eq!(mod_2, reg_mod_2.mod_files[0]); // Tests if bool was parsed correctly assert_eq!(mod_1_state, reg_mod_1.state); diff --git a/tests/test_lib.rs b/tests/test_lib.rs index aa341ab..f674762 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -35,14 +35,14 @@ mod tests { let test_mod = RegMod::new("Test", true, test_files.clone()); let test_files_disabled = test_mod - .files + .mod_files .iter() .map(|file| PathBuf::from(format!("{}.disabled", file.display()))) .collect::>(); - assert_eq!(test_mod.files.len(), 4); + assert_eq!(test_mod.mod_files.len(), 1); assert_eq!(test_mod.config_files.len(), 1); - assert_eq!(test_mod.other_files.len(), 1); + assert_eq!(test_mod.other_files.len(), 4); for test_file in test_files.iter() { File::create(test_file.to_string_lossy().to_string()).unwrap(); @@ -63,7 +63,7 @@ mod tests { let test_mod = RegMod { name: test_mod.name, state: false, - files: test_files_disabled, + mod_files: test_files_disabled, config_files: test_mod.config_files, other_files: test_mod.other_files, }; diff --git a/ui/common.slint b/ui/common.slint index 69d25f4..9a32c09 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -3,8 +3,8 @@ export struct DisplayMod { name: string, enabled: bool, files: [StandardListViewItem], - has-config: bool, config-files: [string], + dll-files: [string], } export enum Message { confirm, deny, esc } diff --git a/ui/tabs.slint b/ui/tabs.slint index 52f5ce6..e6d7a56 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -44,8 +44,9 @@ export component ModDetails inherits Tab { export component ModEdit inherits Tab { in property mod-index; - property button-width: MainLogic.current-mods[mod-index].has-config ? 96px : 105px; - property button-layout: MainLogic.current-mods[mod-index].has-config ? space-between : end; + property has-config: MainLogic.current-mods[mod-index].config-files.length > 0; + property button-width: has-config ? 96px : 105px; + property button-layout: has-config ? space-between : end; VerticalLayout { y: 0px; padding-left: Formatting.side-padding; @@ -68,7 +69,7 @@ export component ModEdit inherits Tab { text: @tr("Add Files"); clicked => { MainLogic.add-to-mod(MainLogic.current-mods[mod-index].name) } } - if (MainLogic.current-mods[mod-index].has-config) : Button { + if (has-config) : Button { width: button-width; height: 35px; primary: !SettingsLogic.dark-mode; From 5c3004340820213688e4bec384f505739426f7ac Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 17:38:14 -0500 Subject: [PATCH 11/62] Cleaned up FileData methods added convinence methods for checking if any file is in the enabled or disabled state, this make the calls look much cleaner and easier to read also added a sucess dialog for adding files to a mod --- src/lib.rs | 33 +++++++++++++++++---------------- src/main.rs | 15 ++++++++------- src/utils/ini/parser.rs | 12 ++---------- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index be845c4..0b93cf8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,33 +255,34 @@ pub struct FileData<'a> { impl FileData<'_> { pub fn from(name: &str) -> FileData { - match name.find(".disabled") { - Some(index) => { + let off_state = ".disabled"; + if let Some(index) = name.find(off_state) { + if index == name.len() - off_state.len() { let first_split = name.split_at(name[..index].rfind('.').expect("is file")); - FileData { + return FileData { name: first_split.0, extension: first_split .1 .split_at(first_split.1.rfind('.').expect("ends in .disabled")) .0, enabled: false, - } - } - - None => { - let split = name.split_at(name.rfind('.').expect("is file")); - FileData { - name: split.0, - extension: split.1, - enabled: true, - } + }; } } + let split = name.split_at(name.rfind('.').expect("is file")); + FileData { + name: split.0, + extension: split.1, + enabled: true, + } + } + + pub fn is_enabled>(path: &T) -> bool { + FileData::from(&path.as_ref().to_string_lossy()).enabled } - pub fn is_enabled(path: &Path) -> std::io::Result { - let file_name = file_name_or_err(path)?.to_string_lossy(); - Ok(FileData::from(&file_name).enabled) + pub fn is_disabled>(path: &T) -> bool { + !FileData::from(&path.as_ref().to_string_lossy()).enabled } } diff --git a/src/main.rs b/src/main.rs index 4c89ced..50cd5f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -289,9 +289,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg("A selected file is already registered to a mod"); return; } - let state = !files.iter().all(|file| { - matches!(FileData::is_enabled(file), Ok(false)) - }); + let state = !files.iter().all(FileData::is_disabled); results.push(save_bool( current_ini, Some("registered-mods"), @@ -535,9 +533,10 @@ fn main() -> Result<(), slint::PlatformError> { if file_registered(®istered_mods, &files) { ui.display_msg("A selected file is already registered to a mod"); } else { + let num_files = files.len(); let mut new_data = found_mod.mod_files.clone(); new_data.extend(files); - let mut results = Vec::with_capacity(2); + let mut results = Vec::with_capacity(3); let new_data_refs = found_mod.add_other_files_to_files(&new_data); if found_mod.all_files_len() == 1 { results.push(remove_entry( @@ -574,7 +573,11 @@ fn main() -> Result<(), slint::PlatformError> { &updated_mod.name, ); }; + results.push(Err(err)); }); + if !results.iter().any(|r| r.is_err()) { + ui.display_msg(&format!("Sucessfully added {} file(s) to {}", num_files, format_key)); + } ui.global::().set_current_mods(deserialize( &RegMod::collect(current_ini, false).unwrap_or_else(|_| { match RegMod::collect(current_ini, false) { @@ -620,9 +623,7 @@ fn main() -> Result<(), slint::PlatformError> { reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { let mut found_files = found_mod.mod_files.clone(); - if found_files.iter().any(|file| { - matches!(FileData::is_enabled(file), Ok(false)) - }) { + if found_files.iter().any(FileData::is_disabled) { match toggle_files(&game_dir, true, found_mod, Some(current_ini)) { Ok(files) => found_files = files, Err(err) => { diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 73b1355..d60eed1 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -444,16 +444,8 @@ impl RegMod { } } pub fn verify_state(&self, game_dir: &Path, ini_file: &Path) -> std::io::Result<()> { - if (!self.state - && self - .mod_files - .iter() - .any(|path| matches!(FileData::is_enabled(path), Ok(true)))) - || (self.state - && self - .mod_files - .iter() - .any(|path| matches!(FileData::is_enabled(path), Ok(false)))) + if (!self.state && self.mod_files.iter().any(FileData::is_enabled)) + || (self.state && self.mod_files.iter().any(FileData::is_disabled)) { warn!( "wrong file state for \"{}\" chaning file extentions", From 3fe3a663ae573ca4dbbd93ea303d1d6b15e15964 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 17:39:42 -0500 Subject: [PATCH 12/62] Cargo update --- Cargo.lock | 56 +++++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76333d7..7dabe48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -370,8 +370,8 @@ dependencies = [ "futures-io", "futures-lite 2.3.0", "parking", - "polling 3.6.0", - "rustix 0.38.33", + "polling 3.7.0", + "rustix 0.38.34", "slab", "tracing", "windows-sys 0.52.0", @@ -421,7 +421,7 @@ dependencies = [ "cfg-if", "event-listener 3.1.0", "futures-lite 1.13.0", - "rustix 0.38.33", + "rustix 0.38.34", "windows-sys 0.48.0", ] @@ -440,7 +440,7 @@ dependencies = [ "cfg-if", "event-listener 5.3.0", "futures-lite 2.3.0", - "rustix 0.38.33", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -468,7 +468,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 0.38.33", + "rustix 0.38.34", "signal-hook-registry", "slab", "windows-sys 0.52.0", @@ -737,8 +737,8 @@ checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" dependencies = [ "bitflags 2.5.0", "log", - "polling 3.6.0", - "rustix 0.38.33", + "polling 3.7.0", + "rustix 0.38.34", "slab", "thiserror", ] @@ -750,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" dependencies = [ "calloop", - "rustix 0.38.33", + "rustix 0.38.34", "wayland-backend", "wayland-client", ] @@ -2486,9 +2486,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" dependencies = [ "libc", ] @@ -3171,15 +3171,15 @@ dependencies = [ [[package]] name = "polling" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "645493cf344456ef24219d02a768cf1fb92ddf8c92161679ae3d91b91a637be3" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 0.38.33", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -3503,9 +3503,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.33" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3cc72858054fcff6d7dea32df2aeaee6a7c24227366d7ea429aada2f26b16ad" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ "bitflags 2.5.0", "errno", @@ -3810,7 +3810,7 @@ dependencies = [ "libc", "log", "memmap2 0.9.4", - "rustix 0.38.33", + "rustix 0.38.34", "thiserror", "wayland-backend", "wayland-client", @@ -3871,7 +3871,7 @@ dependencies = [ "objc", "raw-window-handle 0.5.2", "redox_syscall 0.4.1", - "rustix 0.38.33", + "rustix 0.38.34", "tiny-xlib", "wasm-bindgen", "wayland-backend", @@ -3994,7 +3994,7 @@ checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand 2.0.2", - "rustix 0.38.33", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -4524,7 +4524,7 @@ checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", - "rustix 0.38.33", + "rustix 0.38.34", "scoped-tls", "smallvec", "wayland-sys", @@ -4537,7 +4537,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ "bitflags 2.5.0", - "rustix 0.38.33", + "rustix 0.38.34", "wayland-backend", "wayland-scanner", ] @@ -4559,7 +4559,7 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71ce5fa868dd13d11a0d04c5e2e65726d0897be8de247c0c5a65886e283231ba" dependencies = [ - "rustix 0.38.33", + "rustix 0.38.34", "wayland-client", "xcursor", ] @@ -4670,7 +4670,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix 0.38.33", + "rustix 0.38.34", ] [[package]] @@ -4691,11 +4691,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "134306a13c5647ad6453e8deaec55d3a44d6021970129e6188735e74bf546697" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -5020,7 +5020,7 @@ dependencies = [ "raw-window-handle 0.5.2", "raw-window-handle 0.6.1", "redox_syscall 0.3.5", - "rustix 0.38.33", + "rustix 0.38.34", "sctk-adwaita", "smithay-client-toolkit", "smol_str", @@ -5125,7 +5125,7 @@ dependencies = [ "libc", "libloading 0.8.3", "once_cell", - "rustix 0.38.33", + "rustix 0.38.34", "x11rb-protocol 0.13.0", ] @@ -5152,7 +5152,7 @@ checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", "linux-raw-sys 0.4.13", - "rustix 0.38.33", + "rustix 0.38.34", ] [[package]] From 48ed8d86d6e1b57d21fb0f835d47fccad1e2a9bd Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 17:40:06 -0500 Subject: [PATCH 13/62] Small bug fixes in UI --- ui/appwindow.slint | 4 ++-- ui/tabs.slint | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/appwindow.slint b/ui/appwindow.slint index 15d1227..b79d45f 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -15,8 +15,8 @@ export component App inherits Window { // window-height = main-page-height -? page-title-height property window-height: mp.height - Formatting.header-height; property popup-window-x-pos: { - if ((mp.width - popup-window-width) / 2) < 10px { - 10px + if ((mp.width - popup-window-width) / 2) < Formatting.side-padding - 1px { + Formatting.side-padding - 1px } else { (mp.width - popup-window-width) / 2 } diff --git a/ui/tabs.slint b/ui/tabs.slint index e6d7a56..034da96 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -35,7 +35,7 @@ export component ModDetails inherits Tab { width: Formatting.group-box-width - Formatting.side-padding; model: MainLogic.current-mods[mod-index].files; item-pointer-event(i, event) => { - if event.kind == PointerEventKind.up { + if event.kind == PointerEventKind.up && event.button == PointerEventButton.left { MainLogic.edit-config-item(MainLogic.current-mods[mod-index].files[i]) } } From b7074dce86b009f715dc1cf59a474c6c70c42bed Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 20:36:47 -0500 Subject: [PATCH 14/62] Much faster access to game_dir We no longer rely on grabbing the verified game_dir from the front end Now we use a OnceLock> to hold and update the game_dir This allows much faster access the the data --- src/main.rs | 49 +++++++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index 50cd5f5..fd724df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -#![windows_subsystem = "windows"] +// #![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ @@ -32,6 +32,7 @@ const CONFIG_NAME: &str = "EML_gui_config.ini"; static GLOBAL_NUM_KEY: AtomicU32 = AtomicU32::new(0); static RESTRICTED_FILES: OnceLock<[&'static OsStr; 6]> = OnceLock::new(); static RECEIVER: OnceLock>> = OnceLock::new(); +static GAME_DIR: OnceLock> = OnceLock::new(); fn main() -> Result<(), slint::PlatformError> { env_logger::init(); @@ -128,6 +129,7 @@ fn main() -> Result<(), slint::PlatformError> { .to_string() .into(), ); + GAME_DIR.set(RwLock::new(game_dir.clone().unwrap_or_default())).unwrap(); ui.global::().set_current_mods(deserialize( &RegMod::collect(current_ini, !game_verified).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); @@ -242,8 +244,8 @@ fn main() -> Result<(), slint::PlatformError> { return; } } - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); slint::spawn_local(async move { + let game_dir = GAME_DIR.get().unwrap().read().await; let file_paths = match get_user_files(&game_dir) { Ok(files) => files, Err(err) => { @@ -353,9 +355,10 @@ fn main() -> Result<(), slint::PlatformError> { move || { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); slint::spawn_local(async move { + let game_dir = GAME_DIR.get().unwrap().read().await; let path_result = get_user_folder(&game_dir); + drop(game_dir); let path = match path_result { Ok(path) => path, Err(err) => { @@ -395,6 +398,7 @@ fn main() -> Result<(), slint::PlatformError> { }; ui.global::() .set_game_path(try_path.to_string_lossy().to_string().into()); + update_game_dir(try_path).await; ui.global::().set_game_path_valid(true); ui.global::().set_current_subpage(0); ui.global::().set_loader_installed(mod_loader.installed); @@ -426,7 +430,7 @@ fn main() -> Result<(), slint::PlatformError> { move |key, state| { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let format_key = key.replace(' ', "_"); match RegMod::collect(current_ini, false) { Ok(reg_mods) => { @@ -473,10 +477,6 @@ fn main() -> Result<(), slint::PlatformError> { move |key| { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); - let format_key = key.replace(' ', "_"); - let game_dir = PathBuf::from( - ui.global::().get_game_path().to_string(), - ); let registered_mods = match RegMod::collect(current_ini, false) { Ok(data) => data, Err(err) => { @@ -485,6 +485,8 @@ fn main() -> Result<(), slint::PlatformError> { } }; slint::spawn_local(async move { + let game_dir = GAME_DIR.get().unwrap().read().await; + let format_key = key.replace(' ', "_"); let file_paths = match get_user_files(&game_dir) { Ok(paths) => paths, Err(err) => { @@ -617,8 +619,7 @@ fn main() -> Result<(), slint::PlatformError> { } }; - let game_dir = - PathBuf::from(ui.global::().get_game_path().to_string()); + let game_dir = GAME_DIR.get().unwrap().read().await; if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { @@ -681,12 +682,12 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |config_item| { let ui = ui_handle.unwrap(); - let game_dir = ui.global::().get_game_path(); + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let item = config_item.text.to_string(); if !matches!(FileData::from(&item).extension, ".txt" | ".ini") { return; }; - let os_file = vec![std::ffi::OsString::from(format!("{game_dir}\\{item}"))]; + let os_file = vec![std::ffi::OsString::from(format!("{}\\{item}", game_dir.display()))]; open_text_files(ui.as_weak(), os_file); } }); @@ -694,14 +695,14 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |config_file| { let ui = ui_handle.unwrap(); - let game_dir = ui.global::().get_game_path(); + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let downcast_config_file = config_file .as_any() .downcast_ref::>() .expect("We know we set a VecModel earlier"); let os_files = downcast_config_file .iter() - .map(|path| std::ffi::OsString::from(format!("{game_dir}\\{path}"))) + .map(|path| std::ffi::OsString::from(format!("{}\\{path}", game_dir.display()))) .collect::>(); open_text_files(ui.as_weak(), os_files); } @@ -711,8 +712,7 @@ fn main() -> Result<(), slint::PlatformError> { move |state| { let ui = ui_handle.unwrap(); let value = if state { "1" } else { "0" }; - let ext_ini = PathBuf::from(ui.global::().get_game_path().to_string()) - .join(LOADER_FILES[0]); + let ext_ini = GAME_DIR.get().unwrap().blocking_read().join(LOADER_FILES[0]); save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( |err| { ui.display_msg(&err.to_string()); @@ -725,8 +725,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |time| { let ui = ui_handle.unwrap(); - let ext_ini = PathBuf::from(ui.global::().get_game_path().to_string()) - .join(LOADER_FILES[0]); + let ext_ini = GAME_DIR.get().unwrap().blocking_read().join(LOADER_FILES[0]); ui.global::().invoke_force_app_focus(); if let Err(err) = save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { ui.display_msg(&format!("Failed to set load delay\n\n{err}")); @@ -742,7 +741,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |state| { let ui = ui_handle.unwrap(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let files = if state { vec![PathBuf::from(LOADER_FILES[1])] } else { @@ -762,9 +761,9 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - let game_dir = ui.global::().get_game_path().to_string(); let jh = std::thread::spawn(move || { - std::process::Command::new("explorer").arg(game_dir).spawn() + let game_dir = GAME_DIR.get().unwrap().blocking_read(); + std::process::Command::new("explorer").arg(game_dir.as_path()).spawn() }); match jh.join() { Ok(result) => match result { @@ -796,7 +795,7 @@ fn main() -> Result<(), slint::PlatformError> { let current_ini = get_ini_dir(); slint::spawn_local(async move { let ui_handle = ui.as_weak(); - let game_dir = PathBuf::from(ui.global::().get_game_path().to_string()); + let game_dir = GAME_DIR.get().unwrap().read().await; match confirm_scan_mods(ui_handle, &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); @@ -891,6 +890,12 @@ fn get_ini_dir() -> &'static PathBuf { }) } +async fn update_game_dir(path: PathBuf) { + let gd = GAME_DIR.get().unwrap(); + let mut gd_lock = gd.write().await; + *gd_lock = path; +} + fn populate_restricted_files() -> [&'static OsStr; 6] { let mut restricted_files: [&OsStr; 6] = [&OsStr::new(""); 6]; for (i, file) in LOADER_FILES.iter().map(OsStr::new).enumerate() { From a6020ebb729dde562c506d9c9a6dab23f93001ef Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 21:39:43 -0500 Subject: [PATCH 15/62] updated feature permission reqs upgraded togglemod and import mod to req - mod-loader-installed = true --- ui/appwindow.slint | 1 + ui/main.slint | 12 ++++++------ ui/settings.slint | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/ui/appwindow.slint b/ui/appwindow.slint index b79d45f..b2289e1 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -21,6 +21,7 @@ export component App inherits Window { (mp.width - popup-window-width) / 2 } }; + // Still possible to run into the - px range property popup-window-y-pos: { if ((window-height - popup-window-height) / 2) < 0px { 60px diff --git a/ui/main.slint b/ui/main.slint index 5ddf494..6b96e59 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -32,7 +32,7 @@ export component MainPage inherits Page { reg-mod-box := GroupBox { title: @tr("Registered-Mods:"); - enabled: MainLogic.game-path-valid && !SettingsLogic.loader-disabled; + enabled: SettingsLogic.loader-installed && !SettingsLogic.loader-disabled; list-view := ListView { for mod[idx] in MainLogic.current-mods: re := Rectangle { @@ -83,12 +83,12 @@ export component MainPage inherits Page { } } } - GroupBox { + add-mod-box := GroupBox { height: 85px; title: @tr("Add Mod"); - enabled: MainLogic.game-path-valid; + enabled: SettingsLogic.loader-installed; FocusScope { - enabled: parent.enabled; + enabled: add-mod-box.enabled; key-pressed(event) => { if (event.text == Key.Escape) { MainLogic.force-app-focus() @@ -103,7 +103,7 @@ export component MainPage inherits Page { input-mod := LineEdit { height: 35px; placeholder-text: @tr("Mod Name"); - enabled: MainLogic.game-path-valid; + enabled: add-mod-box.enabled; text <=> MainLogic.line-edit-text; } add-mod := Button { @@ -111,7 +111,7 @@ export component MainPage inherits Page { width: 95px; text: @tr("Select Files"); primary: !SettingsLogic.dark-mode; - enabled: MainLogic.game-path-valid; + enabled: add-mod-box.enabled; clicked => { if(input-mod.text != "") { MainLogic.force-app-focus(); diff --git a/ui/settings.slint b/ui/settings.slint index 9a9d065..004feee 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -73,7 +73,7 @@ export component SettingsPage inherits Page { clicked => { SettingsLogic.open-game-dir() } } Button { - width: 105px; + width: 106px; height: 30px; primary: !SettingsLogic.dark-mode; text: @tr("Set Path"); From ec226222b741e5a54cb1a6c81c2f756a4a5ab14a Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 21:41:09 -0500 Subject: [PATCH 16/62] moved to all game_dir blocking reads this seems to improve stability --- src/main.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index fd724df..ee3ab01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -// #![windows_subsystem = "windows"] +#![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ @@ -178,7 +178,7 @@ fn main() -> Result<(), slint::PlatformError> { } } if !first_startup && !mod_loader.installed { - ui.display_msg("This tool requires Elden Mod Loader by TechieW to be installed!"); + ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", &game_dir.display())); } } if first_startup { @@ -245,7 +245,7 @@ fn main() -> Result<(), slint::PlatformError> { } } slint::spawn_local(async move { - let game_dir = GAME_DIR.get().unwrap().read().await; + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let file_paths = match get_user_files(&game_dir) { Ok(files) => files, Err(err) => { @@ -356,7 +356,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); slint::spawn_local(async move { - let game_dir = GAME_DIR.get().unwrap().read().await; + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let path_result = get_user_folder(&game_dir); drop(game_dir); let path = match path_result { @@ -485,7 +485,7 @@ fn main() -> Result<(), slint::PlatformError> { } }; slint::spawn_local(async move { - let game_dir = GAME_DIR.get().unwrap().read().await; + let game_dir = GAME_DIR.get().unwrap().blocking_read(); let format_key = key.replace(' ', "_"); let file_paths = match get_user_files(&game_dir) { Ok(paths) => paths, @@ -619,7 +619,7 @@ fn main() -> Result<(), slint::PlatformError> { } }; - let game_dir = GAME_DIR.get().unwrap().read().await; + let game_dir = GAME_DIR.get().unwrap().blocking_read(); if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { @@ -795,7 +795,7 @@ fn main() -> Result<(), slint::PlatformError> { let current_ini = get_ini_dir(); slint::spawn_local(async move { let ui_handle = ui.as_weak(); - let game_dir = GAME_DIR.get().unwrap().read().await; + let game_dir = GAME_DIR.get().unwrap().blocking_read(); match confirm_scan_mods(ui_handle, &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); From 7c9fa64c5ac41c0ce4e55b3511549ad8ef740f22 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 21:57:40 -0500 Subject: [PATCH 17/62] Moved off_state to a lib.rs const --- src/lib.rs | 13 ++++++------- src/main.rs | 2 +- tests/test_lib.rs | 5 +++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0b93cf8..640cef4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,6 +32,7 @@ pub const REQUIRED_GAME_FILES: [&str; 3] = [ "eossdk-win64-shipping.dll", ]; +pub const OFF_STATE: &str = ".disabled"; pub const LOADER_FILES: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll"]; pub const LOADER_FILES_DISABLED: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll.disabled"]; pub const LOADER_SECTIONS: [Option<&str>; 2] = [Some("modloader"), Some("loadorder")]; @@ -132,18 +133,17 @@ pub fn toggle_files( file_paths .iter() .map(|path| { - let off_state = ".disabled"; let file_name = match path.file_name() { Some(name) => name, None => path.as_os_str(), }; let mut new_name = file_name.to_string_lossy().to_string(); - if let Some(index) = new_name.to_lowercase().find(off_state) { + if let Some(index) = new_name.to_lowercase().find(OFF_STATE) { if new_state { - new_name.replace_range(index..index + off_state.len(), ""); + new_name.replace_range(index..index + OFF_STATE.len(), ""); } } else if !new_state { - new_name.push_str(off_state); + new_name.push_str(OFF_STATE); } let mut new_path = path.clone(); new_path.set_file_name(new_name); @@ -255,9 +255,8 @@ pub struct FileData<'a> { impl FileData<'_> { pub fn from(name: &str) -> FileData { - let off_state = ".disabled"; - if let Some(index) = name.find(off_state) { - if index == name.len() - off_state.len() { + if let Some(index) = name.find(OFF_STATE) { + if index == name.len() - OFF_STATE.len() { let first_split = name.split_at(name[..index].rfind('.').expect("is file")); return FileData { name: first_split.0, diff --git a/src/main.rs b/src/main.rs index ee3ab01..0e8a6f1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -944,7 +944,7 @@ fn deserialize(data: &[RegMod]) -> ModelRc { let dll_files: Rc> = Default::default(); let config_files: Rc> = Default::default(); if !mod_data.mod_files.is_empty() { - files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(".disabled", "")).into())); + files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); dll_files.extend(mod_data.mod_files.iter().map(|file|SharedString::from(file.to_string_lossy().to_string()))); }; if !mod_data.config_files.is_empty() { diff --git a/tests/test_lib.rs b/tests/test_lib.rs index f674762..77216b1 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -1,8 +1,9 @@ #[cfg(test)] mod tests { use elden_mod_loader_gui::{ + toggle_files, utils::ini::{parser::RegMod, writer::new_cfg}, - *, + OFF_STATE, }; use std::{ fs::{metadata, remove_file, File}, @@ -37,7 +38,7 @@ mod tests { let test_files_disabled = test_mod .mod_files .iter() - .map(|file| PathBuf::from(format!("{}.disabled", file.display()))) + .map(|file| PathBuf::from(format!("{}{OFF_STATE}", file.display()))) .collect::>(); assert_eq!(test_mod.mod_files.len(), 1); From a9361da95d6c17376670094d1dc78e574b90e88d Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 24 Apr 2024 22:46:46 -0500 Subject: [PATCH 18/62] Updated OnceLock game_dir interface update and retrieve game_dir with the same function I find this looks cleaner --- src/lib.rs | 2 ++ src/main.rs | 45 ++++++++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 640cef4..4d2096d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -254,6 +254,8 @@ pub struct FileData<'a> { } impl FileData<'_> { + /// To get an accurate FileData.name function input needs .file_name() called before hand + /// FileData.extension && FileData.enabled are accurate with any &Path str as input pub fn from(name: &str) -> FileData { if let Some(index) = name.find(OFF_STATE) { if index == name.len() - OFF_STATE.len() { diff --git a/src/main.rs b/src/main.rs index 0e8a6f1..8073945 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,6 @@ const CONFIG_NAME: &str = "EML_gui_config.ini"; static GLOBAL_NUM_KEY: AtomicU32 = AtomicU32::new(0); static RESTRICTED_FILES: OnceLock<[&'static OsStr; 6]> = OnceLock::new(); static RECEIVER: OnceLock>> = OnceLock::new(); -static GAME_DIR: OnceLock> = OnceLock::new(); fn main() -> Result<(), slint::PlatformError> { env_logger::init(); @@ -129,7 +128,7 @@ fn main() -> Result<(), slint::PlatformError> { .to_string() .into(), ); - GAME_DIR.set(RwLock::new(game_dir.clone().unwrap_or_default())).unwrap(); + let _ = get_or_update_game_dir(Some(game_dir.clone().unwrap_or_default())); ui.global::().set_current_mods(deserialize( &RegMod::collect(current_ini, !game_verified).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); @@ -245,7 +244,7 @@ fn main() -> Result<(), slint::PlatformError> { } } slint::spawn_local(async move { - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let file_paths = match get_user_files(&game_dir) { Ok(files) => files, Err(err) => { @@ -356,7 +355,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); slint::spawn_local(async move { - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let path_result = get_user_folder(&game_dir); drop(game_dir); let path = match path_result { @@ -398,7 +397,7 @@ fn main() -> Result<(), slint::PlatformError> { }; ui.global::() .set_game_path(try_path.to_string_lossy().to_string().into()); - update_game_dir(try_path).await; + let _ = get_or_update_game_dir(Some(try_path)); ui.global::().set_game_path_valid(true); ui.global::().set_current_subpage(0); ui.global::().set_loader_installed(mod_loader.installed); @@ -430,7 +429,7 @@ fn main() -> Result<(), slint::PlatformError> { move |key, state| { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let format_key = key.replace(' ', "_"); match RegMod::collect(current_ini, false) { Ok(reg_mods) => { @@ -485,7 +484,7 @@ fn main() -> Result<(), slint::PlatformError> { } }; slint::spawn_local(async move { - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let format_key = key.replace(' ', "_"); let file_paths = match get_user_files(&game_dir) { Ok(paths) => paths, @@ -619,7 +618,7 @@ fn main() -> Result<(), slint::PlatformError> { } }; - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { @@ -682,7 +681,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |config_item| { let ui = ui_handle.unwrap(); - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let item = config_item.text.to_string(); if !matches!(FileData::from(&item).extension, ".txt" | ".ini") { return; @@ -695,7 +694,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |config_file| { let ui = ui_handle.unwrap(); - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let downcast_config_file = config_file .as_any() .downcast_ref::>() @@ -712,7 +711,7 @@ fn main() -> Result<(), slint::PlatformError> { move |state| { let ui = ui_handle.unwrap(); let value = if state { "1" } else { "0" }; - let ext_ini = GAME_DIR.get().unwrap().blocking_read().join(LOADER_FILES[0]); + let ext_ini = get_or_update_game_dir(None).join(LOADER_FILES[0]); save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( |err| { ui.display_msg(&err.to_string()); @@ -725,7 +724,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |time| { let ui = ui_handle.unwrap(); - let ext_ini = GAME_DIR.get().unwrap().blocking_read().join(LOADER_FILES[0]); + let ext_ini = get_or_update_game_dir(None).join(LOADER_FILES[0]); ui.global::().invoke_force_app_focus(); if let Err(err) = save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { ui.display_msg(&format!("Failed to set load delay\n\n{err}")); @@ -741,7 +740,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |state| { let ui = ui_handle.unwrap(); - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); let files = if state { vec![PathBuf::from(LOADER_FILES[1])] } else { @@ -762,7 +761,7 @@ fn main() -> Result<(), slint::PlatformError> { move || { let ui = ui_handle.unwrap(); let jh = std::thread::spawn(move || { - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); std::process::Command::new("explorer").arg(game_dir.as_path()).spawn() }); match jh.join() { @@ -795,7 +794,7 @@ fn main() -> Result<(), slint::PlatformError> { let current_ini = get_ini_dir(); slint::spawn_local(async move { let ui_handle = ui.as_weak(); - let game_dir = GAME_DIR.get().unwrap().blocking_read(); + let game_dir = get_or_update_game_dir(None); match confirm_scan_mods(ui_handle, &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); @@ -890,10 +889,18 @@ fn get_ini_dir() -> &'static PathBuf { }) } -async fn update_game_dir(path: PathBuf) { - let gd = GAME_DIR.get().unwrap(); - let mut gd_lock = gd.write().await; - *gd_lock = path; +fn get_or_update_game_dir(update: Option) -> tokio::sync::RwLockReadGuard<'static, std::path::PathBuf> { + static GAME_DIR: OnceLock> = OnceLock::new(); + + if let Some(path) = update { + let gd = GAME_DIR.get_or_init(|| { + RwLock::new(path.clone()) + }); + let mut gd_lock = gd.blocking_write(); + *gd_lock = path; + } + + GAME_DIR.get().unwrap().blocking_read() } fn populate_restricted_files() -> [&'static OsStr; 6] { From df25787997f9e249eeb61068cb9a7127da19cf2a Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 28 Apr 2024 14:36:57 -0500 Subject: [PATCH 19/62] Feat: set a mods load order This new file handles the logic for setting and updating the order of a mods set load order from the front end. other code dealing with mod_loader files was moved here as well more work needs to be done on this Feat, need to sill parse and deserialize data from io read to the front end to update changes that have been made --- src/utils/ini/mod_loader.rs | 143 ++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/utils/ini/mod_loader.rs diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs new file mode 100644 index 0000000..7ae5b11 --- /dev/null +++ b/src/utils/ini/mod_loader.rs @@ -0,0 +1,143 @@ +use ini::Ini; +use log::{error, info, warn}; +use std::path::{Path, PathBuf}; + +use crate::{ + utils::ini::writer::EXT_OPTIONS, + {does_dir_contain, get_cfg, Operation, LOADER_FILES, LOADER_FILES_DISABLED}, +}; + +#[derive(Default)] +pub struct ModLoader { + pub installed: bool, + pub disabled: bool, + pub cfg: PathBuf, +} + +impl ModLoader { + pub fn properties(game_dir: &Path) -> std::io::Result { + let disabled: bool; + let cfg: PathBuf; + let installed = match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { + Ok(true) => { + info!("Found mod loader files"); + cfg = game_dir.join(LOADER_FILES[0]); + disabled = false; + true + } + Ok(false) => { + warn!("Checking if mod loader is disabled"); + match does_dir_contain(game_dir, Operation::All, &LOADER_FILES_DISABLED) { + Ok(true) => { + info!("Found mod loader files in the disabled state"); + cfg = game_dir.join(LOADER_FILES[0]); + disabled = true; + true + } + Ok(false) => { + error!("Mod Loader Files not found in selected path"); + cfg = PathBuf::new(); + disabled = false; + false + } + Err(err) => return Err(err), + } + } + Err(err) => return Err(err), + }; + Ok(ModLoader { + installed, + disabled, + cfg, + }) + } +} + +pub struct ModLoaderCfg { + cfg: Ini, + cfg_dir: PathBuf, + section: Option, +} + +impl ModLoaderCfg { + pub fn load(game_dir: &Path, section: Option<&str>) -> Result { + if section.is_none() { + return Err(String::from("section can not be none")); + } + let cfg_dir = match does_dir_contain(game_dir, Operation::All, &[LOADER_FILES[0]]) { + Ok(true) => game_dir.join(LOADER_FILES[0]), + Ok(false) => { + return Err(String::from( + "\"mod_loader_config.ini\" does not exist in the current game_dir", + )) + } + Err(err) => return Err(err.to_string()), + }; + let mut cfg = match get_cfg(&cfg_dir) { + Ok(ini) => ini, + Err(err) => return Err(format!("Could not read \"mod_loader_config.ini\"\n{err}")), + }; + if cfg.section(section).is_none() { + cfg.with_section(section).set("setter_temp_val", "0"); + if cfg.delete_from(section, "setter_temp_val").is_none() { + return Err(format!( + "Failed to create a new section: \"{}\"", + section.unwrap() + )); + }; + } + Ok(ModLoaderCfg { + cfg, + cfg_dir, + section: section.map(String::from), + }) + } + + pub fn mut_section(&mut self) -> &mut ini::Properties { + self.cfg.section_mut(self.section.as_ref()).unwrap() + } + + pub fn dir(&self) -> &Path { + &self.cfg_dir + } + + pub fn write_to_file(&self) -> std::io::Result<()> { + self.cfg.write_to_file_opt(&self.cfg_dir, EXT_OPTIONS) + } +} + +pub fn update_order_entries( + stable: Option<&str>, + section: &mut ini::Properties, +) -> Result<(), std::num::ParseIntError> { + let mut k_v = Vec::with_capacity(section.len()); + let (mut stable_k, mut stable_v) = (String::new(), 0_usize); + for (k, v) in section.clone() { + section.remove(&k); + if let Some(new_k) = stable { + if k == new_k { + (stable_k, stable_v) = (k, v.parse::()?); + continue; + } + } + k_v.push((k, v.parse::()?)); + } + k_v.sort_by_key(|(_, v)| *v); + if k_v.is_empty() && !stable_k.is_empty() { + section.append(&stable_k, "0"); + } else { + let mut offset = 0_usize; + for (k, _) in k_v { + if !stable_k.is_empty() && !section.contains_key(&stable_k) && stable_v == offset { + section.append(&stable_k, stable_v.to_string()); + offset += 1; + } + section.append(k, offset.to_string()); + offset += 1; + } + if !stable_k.is_empty() && !section.contains_key(&stable_k) { + section.append(&stable_k, offset.to_string()) + } + } + Ok(()) +} From 5a2154f4637e2d66cc0ad67d3786f6b359942733 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 28 Apr 2024 14:40:14 -0500 Subject: [PATCH 20/62] impl FileData optimization code dealing with mod_loader was moved to utils/ini/mod_loader.rs --- src/lib.rs | 80 +++++++++++++++++------------------------------------- 1 file changed, 25 insertions(+), 55 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4d2096d..e6dedc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ pub mod utils { pub mod installer; pub mod ini { + pub mod mod_loader; pub mod parser; pub mod writer; } @@ -78,50 +79,6 @@ pub fn shorten_paths(paths: &[PathBuf], remove: &PathBuf) -> Result } } -#[derive(Default)] -pub struct ModLoader { - pub installed: bool, - pub disabled: bool, - pub cfg: PathBuf, -} - -pub fn elden_mod_loader_properties(game_dir: &Path) -> std::io::Result { - let disabled: bool; - let cfg: PathBuf; - let installed = match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { - Ok(true) => { - info!("Found mod loader files"); - cfg = game_dir.join(LOADER_FILES[0]); - disabled = false; - true - } - Ok(false) => { - warn!("Checking if mod loader is disabled"); - match does_dir_contain(game_dir, Operation::All, &LOADER_FILES_DISABLED) { - Ok(true) => { - info!("Found mod loader files in the disabled state"); - cfg = game_dir.join(LOADER_FILES[0]); - disabled = true; - true - } - Ok(false) => { - error!("Mod Loader Files not found in selected path"); - cfg = PathBuf::new(); - disabled = false; - false - } - Err(err) => return Err(err), - } - } - Err(err) => return Err(err), - }; - Ok(ModLoader { - installed, - disabled, - cfg, - }) -} - pub fn toggle_files( game_dir: &Path, new_state: bool, @@ -257,33 +214,46 @@ impl FileData<'_> { /// To get an accurate FileData.name function input needs .file_name() called before hand /// FileData.extension && FileData.enabled are accurate with any &Path str as input pub fn from(name: &str) -> FileData { - if let Some(index) = name.find(OFF_STATE) { - if index == name.len() - OFF_STATE.len() { + match FileData::state_data(name) { + (false, index) => { let first_split = name.split_at(name[..index].rfind('.').expect("is file")); - return FileData { + FileData { name: first_split.0, extension: first_split .1 .split_at(first_split.1.rfind('.').expect("ends in .disabled")) .0, enabled: false, - }; + } + } + (true, _) => { + let split = name.split_at(name.rfind('.').expect("is file")); + FileData { + name: split.0, + extension: split.1, + enabled: true, + } } } - let split = name.split_at(name.rfind('.').expect("is file")); - FileData { - name: split.0, - extension: split.1, - enabled: true, + } + + #[inline] + fn state_data(path: &str) -> (bool, usize) { + if let Some(index) = path.find(OFF_STATE) { + (index != path.len() - OFF_STATE.len(), index) + } else { + (true, 0) } } + #[inline] pub fn is_enabled>(path: &T) -> bool { - FileData::from(&path.as_ref().to_string_lossy()).enabled + FileData::state_data(&path.as_ref().to_string_lossy()).0 } + #[inline] pub fn is_disabled>(path: &T) -> bool { - !FileData::from(&path.as_ref().to_string_lossy()).enabled + !FileData::state_data(&path.as_ref().to_string_lossy()).0 } } From 3a4ca48489d1f2e3d002384de15b1a18d25fa0d3 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 28 Apr 2024 14:41:45 -0500 Subject: [PATCH 21/62] inital logic impl for load_order fns --- src/main.rs | 104 ++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8073945..c38b963 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,11 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -#![windows_subsystem = "windows"] +// #![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ ini::{ + mod_loader::{ModLoader, ModLoaderCfg, update_order_entries}, parser::{file_registered, IniProperty, RegMod, Valitidity}, writer::*, }, @@ -145,8 +146,8 @@ fn main() -> Result<(), slint::PlatformError> { } mod_loader = ModLoader::default(); } else { - let game_dir = game_dir.clone().expect("game dir verified"); - mod_loader = elden_mod_loader_properties(&game_dir).unwrap_or_default(); + let game_dir = game_dir.expect("game dir verified"); + mod_loader = ModLoader::properties(&game_dir).unwrap_or_default(); ui.global::() .set_loader_disabled(mod_loader.disabled); if mod_loader.installed { @@ -191,8 +192,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); slint::spawn_local(async move { let ui = ui_handle.unwrap(); - let ui_handle = ui.as_weak(); - match confirm_scan_mods(ui_handle, &game_dir.expect("game verified"), current_ini, false).await { + match confirm_scan_mods(ui.as_weak(), &get_or_update_game_dir(None), current_ini, false).await { Ok(len) => { ui.global::().set_current_mods(deserialize( &RegMod::collect(current_ini, false).unwrap_or_else(|err| { @@ -387,7 +387,7 @@ fn main() -> Result<(), slint::PlatformError> { return; }; info!("Success: Files found, saved diretory"); - let mod_loader = match elden_mod_loader_properties(&try_path) { + let mod_loader = match ModLoader::properties(&try_path) { Ok(loader) => loader, Err(err) => { error!("{err}"); @@ -424,7 +424,7 @@ fn main() -> Result<(), slint::PlatformError> { }).unwrap(); } }); - ui.global::().on_toggleMod({ + ui.global::().on_toggle_mod({ let ui_handle = ui.as_weak(); move |key, state| { let ui = ui_handle.unwrap(); @@ -813,6 +813,82 @@ fn main() -> Result<(), slint::PlatformError> { }).unwrap(); } }); + ui.global::().on_add_remove_order({ + let ui_handle = ui.as_weak(); + move |state, key, value| { + let ui = ui_handle.unwrap(); + let game_dir = get_or_update_game_dir(None); + let mut load_order = match ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) { + Ok(data) => data, + Err(err) => { + ui.display_msg(&err); + return; + } + }; + let load_orders = load_order.mut_section(); + let stable_k = match state { + true => { + load_orders.insert(&key, &value.to_string()); + Some(key.as_str()) + } + false => { + if !load_orders.contains_key(&key) { + return; + } + load_orders.remove(&key); + None + } + }; + update_order_entries(stable_k, load_orders).unwrap_or_else(|err| { + ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); + std::mem::swap(load_orders, &mut ini::Properties::new()); + }); + // ui.global::().set_current_mods(deserialize( + // &RegMod::collect(&get_ini_dir(), false).unwrap_or_else(|err| { + // ui.display_msg(&err.to_string()); + // vec![RegMod::default()] + // }), + // )); + load_order.write_to_file().unwrap_or_else(|err| { + ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); + }); + } + }); + ui.global::().on_modify_order({ + let ui_handle = ui.as_weak(); + move |to_k, from_k, value| { + let ui = ui_handle.unwrap(); + let game_dir = get_or_update_game_dir(None); + let mut load_order = match ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) { + Ok(data) => data, + Err(err) => { + ui.display_msg(&err); + return; + } + }; + let load_orders = load_order.mut_section(); + if to_k != from_k { + load_orders.remove(from_k); + load_orders.append(&to_k, value.to_string()); + } else { + load_orders.insert(&to_k, value.to_string()) + }; + + update_order_entries(Some(&to_k), load_orders).unwrap_or_else(|err| { + ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); + std::mem::swap(load_orders, &mut ini::Properties::new()); + }); + // ui.global::().set_current_mods(deserialize( + // &RegMod::collect(&get_ini_dir(), false).unwrap_or_else(|err| { + // ui.display_msg(&err.to_string()); + // vec![RegMod::default()] + // }), + // )); + load_order.write_to_file().unwrap_or_else(|err| { + ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); + }); + } + }); ui.invoke_focus_app(); ui.run() @@ -894,7 +970,7 @@ fn get_or_update_game_dir(update: Option) -> tokio::sync::RwLockReadGua if let Some(path) = update { let gd = GAME_DIR.get_or_init(|| { - RwLock::new(path.clone()) + RwLock::new(PathBuf::new()) }); let mut gd_lock = gd.blocking_write(); *gd_lock = path; @@ -952,11 +1028,11 @@ fn deserialize(data: &[RegMod]) -> ModelRc { let config_files: Rc> = Default::default(); if !mod_data.mod_files.is_empty() { files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); - dll_files.extend(mod_data.mod_files.iter().map(|file|SharedString::from(file.to_string_lossy().to_string()))); + dll_files.extend(mod_data.mod_files.iter().map(|f|SharedString::from(f.file_name().unwrap().to_string_lossy().to_string()))); }; if !mod_data.config_files.is_empty() { files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); - config_files.extend(mod_data.config_files.iter().map(|file| SharedString::from(file.to_string_lossy().to_string()))); + config_files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()))); }; if !mod_data.other_files.is_empty() { files.extend(mod_data.other_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); @@ -972,7 +1048,13 @@ fn deserialize(data: &[RegMod]) -> ModelRc { enabled: mod_data.state, files: ModelRc::from(files), config_files: ModelRc::from(config_files), - dll_files: ModelRc::from(dll_files) + dll_files: ModelRc::from(dll_files), + // MARK: TODO + // parse out LoadOrder properties + // need to be able to sort RegMods by load-order then albethabetical + // need a counter for entries in Load-order or order.set == true + // at - front end is 1 index | if set false LoadOrder::default() + order: LoadOrder::default(), }) } ModelRc::from(display_mods) From 2259c8a609a7db0875c0be33acf83972881bf3cb Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 28 Apr 2024 14:42:25 -0500 Subject: [PATCH 22/62] cargo update --- Cargo.lock | 74 ++++++++++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7dabe48..9686105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd884d7c72877a94102c3715f3b1cd09ff4fac28221add3e57cfbe25c236d093" dependencies = [ - "async-fs 2.1.1", + "async-fs 2.1.2", "async-net", "enumflags2", "futures-channel", @@ -284,7 +284,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "258b52a1aa741b9f09783b2d86cf0aeeb617bbf847f6933340a39644227acbdb" dependencies = [ "event-listener 5.3.0", - "event-listener-strategy 0.5.1", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] @@ -297,7 +297,7 @@ checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928" dependencies = [ "concurrent-queue", "event-listener 5.3.0", - "event-listener-strategy 0.5.1", + "event-listener-strategy 0.5.2", "futures-core", "pin-project-lite", ] @@ -310,7 +310,7 @@ checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-lite 2.3.0", "slab", ] @@ -329,9 +329,9 @@ dependencies = [ [[package]] name = "async-fs" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" +checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock 3.3.0", "blocking", @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "async-recursion" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", @@ -476,9 +476,9 @@ dependencies = [ [[package]] name = "async-task" -version = "4.7.0" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" @@ -671,18 +671,16 @@ dependencies = [ [[package]] name = "blocking" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", "async-lock 3.3.0", "async-task", - "fastrand 2.0.2", "futures-io", "futures-lite 2.3.0", "piper", - "tracing", ] [[package]] @@ -963,9 +961,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] @@ -1548,9 +1546,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ "event-listener 5.3.0", "pin-project-lite", @@ -1583,9 +1581,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fdeflate" @@ -1642,9 +1640,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" dependencies = [ "crc32fast", "miniz_oxide", @@ -1782,7 +1780,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-core", "futures-io", "parking", @@ -2642,9 +2640,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -3102,7 +3100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand 2.0.2", + "fastrand 2.1.0", "futures-io", ] @@ -3606,18 +3604,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.199" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" dependencies = [ "proc-macro2", "quote", @@ -3863,7 +3861,7 @@ dependencies = [ "cfg_aliases 0.1.1", "cocoa", "core-graphics", - "fastrand 2.0.2", + "fastrand 2.1.0", "foreign-types", "js-sys", "log", @@ -3993,7 +3991,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.0.2", + "fastrand 2.1.0", "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -4193,7 +4191,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.6", + "winnow 0.6.7", ] [[package]] @@ -4691,9 +4689,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134306a13c5647ad6453e8deaec55d3a44d6021970129e6188735e74bf546697" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ "windows-sys 0.52.0", ] @@ -5050,9 +5048,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" dependencies = [ "memchr", ] @@ -5262,7 +5260,7 @@ checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" dependencies = [ "async-broadcast 0.7.0", "async-executor", - "async-fs 2.1.1", + "async-fs 2.1.2", "async-io 2.3.2", "async-lock 3.3.0", "async-process 2.2.2", From 90f7a06940b5f22c5510a1a716f062486163f0f8 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 28 Apr 2024 14:42:46 -0500 Subject: [PATCH 23/62] UI Over-Haul lots of UI changes, still more work needs to be done, however now we have a modular working tab system for creating a page with multiple tabs to display and switch between mod_details page has been reworked, state data is now persistant on the top of the page so you easily know what state a mod is in. page is now split into two new tabs tab for mod_details and mod_edit got new features: details tab now uses a model of items to create a list this lets a user interact with each file that is associated with a mod now if a file is of type `.ini` or `.txt` they will open up in notepad edit tab now has options for setting load order --- src/utils/ini/writer.rs | 2 +- ui/appwindow.slint | 2 +- ui/common.slint | 16 +++++- ui/editmod.slint | 1 - ui/main.slint | 9 ++-- ui/tabs.slint | 110 +++++++++++++++++++++++++++++++++++----- 6 files changed, 116 insertions(+), 24 deletions(-) diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 230034d..eb03f72 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -21,7 +21,7 @@ const WRITE_OPTIONS: WriteOption = WriteOption { kv_separator: "=", }; -const EXT_OPTIONS: WriteOption = WriteOption { +pub const EXT_OPTIONS: WriteOption = WriteOption { escape_policy: EscapePolicy::Nothing, line_separator: LineSeparator::CRLF, kv_separator: " = ", diff --git a/ui/appwindow.slint b/ui/appwindow.slint index b2289e1..c657ce2 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -22,6 +22,7 @@ export component App inherits Window { } }; // Still possible to run into the - px range + // only noticed on startup popups property popup-window-y-pos: { if ((window-height - popup-window-height) / 2) < 0px { 60px @@ -184,7 +185,6 @@ export component App inherits Window { MainLogic.current-subpage = 0 } } - // && doesn't work in slint conditonal statements if (event.text == Key.Tab) { if (!popup-visible) { if (MainLogic.current-subpage == 0) { diff --git a/ui/common.slint b/ui/common.slint index 9a32c09..42b87c2 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -1,3 +1,9 @@ +struct LoadOrder { + set: bool, + dll: string, + at: int, +} + export struct DisplayMod { displayname: string, name: string, @@ -5,17 +11,21 @@ export struct DisplayMod { files: [StandardListViewItem], config-files: [string], dll-files: [string], + order: LoadOrder, } export enum Message { confirm, deny, esc } export global MainLogic { - callback toggleMod(string, bool); + callback toggle-mod(string, bool); callback select-mod-files(string); callback add-to-mod(string); callback remove-mod(string); callback edit-config([string]); callback edit-config-item(StandardListViewItem); + // consider using a string instead of an int for value + callback add-remove-order(bool, string, int); + callback modify-order(string, string, int); callback force-app-focus(); callback send-message(Message); in property line-edit-text; @@ -23,7 +33,7 @@ export global MainLogic { in-out property game-path-valid; in-out property current-subpage: 0; in-out property <[DisplayMod]> current-mods: [ - {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true}, + {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, order: {set: false}}, ]; } @@ -83,8 +93,10 @@ export global Formatting { out property default-spacing: 3px; out property side-padding: 8px; out property button-spacing: 5px; + out property default-element-height: 35px; out property rectangle-radius: 10px; out property group-box-width: app-width - Formatting.side-padding; + out property group-box-r1-height: 85px; out property font-size-h1: 18pt; out property font-size-h2: 14pt; out property font-size-h3: 10pt; diff --git a/ui/editmod.slint b/ui/editmod.slint index b17404b..607ba73 100644 --- a/ui/editmod.slint +++ b/ui/editmod.slint @@ -22,7 +22,6 @@ export component ModDetailsPage inherits Page { info-text := HorizontalLayout { y: Formatting.header-height - header-offset; - width: Formatting.app-width; height: 27px; padding-right: Formatting.side-padding; Text { diff --git a/ui/main.slint b/ui/main.slint index 6b96e59..a01bea0 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -26,7 +26,6 @@ export component MainPage inherits Page { VerticalLayout { y: 27px; height: parent.height - self.y; - preferred-width: Formatting.app-width; padding: Formatting.side-padding; padding-bottom: Formatting.side-padding / 2; @@ -48,7 +47,7 @@ export component MainPage inherits Page { toggled => { MainLogic.if-err-bool = self.checked; MainLogic.current-mods[idx].enabled = self.checked; - MainLogic.toggleMod(mod.name, self.checked); + MainLogic.toggle-mod(mod.name, self.checked); if (MainLogic.if-err-bool != self.checked) { self.checked = MainLogic.if-err-bool; MainLogic.current-mods[idx].enabled = MainLogic.if-err-bool; @@ -84,7 +83,7 @@ export component MainPage inherits Page { } } add-mod-box := GroupBox { - height: 85px; + height: Formatting.group-box-r1-height; title: @tr("Add Mod"); enabled: SettingsLogic.loader-installed; FocusScope { @@ -101,13 +100,13 @@ export component MainPage inherits Page { HorizontalLayout { spacing: Formatting.button-spacing; input-mod := LineEdit { - height: 35px; + height: Formatting.default-element-height; placeholder-text: @tr("Mod Name"); enabled: add-mod-box.enabled; text <=> MainLogic.line-edit-text; } add-mod := Button { - height: 35px; + height: Formatting.default-element-height; width: 95px; text: @tr("Select Files"); primary: !SettingsLogic.dark-mode; diff --git a/ui/tabs.slint b/ui/tabs.slint index 034da96..06d0685 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -1,4 +1,4 @@ -import { GroupBox, Button, StandardListView } from "std-widgets.slint"; +import { GroupBox, Button, StandardListView, Switch, ComboBox, SpinBox } from "std-widgets.slint"; import { Tab, SettingsLogic, MainLogic, Formatting } from "common.slint"; export component ModDetails inherits Tab { @@ -42,43 +42,125 @@ export component ModDetails inherits Tab { } } +// component ChangeObserver { +// in property i; +// in property value: MainLogic.current-mods[i].order.set; +// // first argument is new value; must return second argument +// pure callback changed(bool, float) -> float; + +// width: 0; height: 0; visible: false; + +// opacity: changed(value, 1); +// } + +// Text { +// property debug: MainLogic.current-mods[mod-index].order.set ? "true" : "false"; +// text: debug; +// } + +// Button { +// text: "test"; +// clicked => {MainLogic.current-mods[mod-index].order.set = !MainLogic.current-mods[mod-index].order.set} +// } + export component ModEdit inherits Tab { in property mod-index; property has-config: MainLogic.current-mods[mod-index].config-files.length > 0; + property selected-order: MainLogic.current-mods[mod-index].order.at; + property selected-dll: MainLogic.current-mods[mod-index].order.dll; property button-width: has-config ? 96px : 105px; property button-layout: has-config ? space-between : end; VerticalLayout { y: 0px; - padding-left: Formatting.side-padding; - alignment: end; + padding: Formatting.side-padding; + padding-bottom: Formatting.side-padding / 2; + alignment: space-between; + + GroupBox { + title: @tr("Load Order"); + HorizontalLayout { + row: 1; + padding-top: Formatting.default-padding; + load-order := Switch { + text: @tr("Set Load Order"); + enabled: MainLogic.current-mods[mod-index].dll-files.length > 0; + // A two way binding on checked would be nice, this would clean up the code and ? keep state in sync ? + checked: MainLogic.current-mods[mod-index].order.set; + toggled => { + if MainLogic.current-mods[mod-index].dll-files.length == 1 && selected-dll == "" { + MainLogic.current-mods[mod-index].order.dll = MainLogic.current-mods[mod-index].dll-files[dll-selection.current-index]; + } + if MainLogic.current-mods[mod-index].order.dll != "" && selected-order > 0 { + // Front end is 1 based and back end is 0 based + MainLogic.add-remove-order(self.checked, selected-dll, selected-order - 1) + } + MainLogic.current-mods[mod-index].order.set = self.checked + } + } + } + HorizontalLayout { + row: 2; + padding-top: Formatting.side-padding; + spacing: Formatting.default-spacing; + dll-selection := ComboBox { + enabled: load-order.checked; + // MARK: TODO + // need to find a way to have current value selected if already enabled + // combo-box is weird if you use current value with a string outside of the model + // sort dll files by name and then store index? + // current-value: selected-dll; + current-index: MainLogic.current-mods[mod-index].dll-files.length == 1 ? 0 : -1; + model: MainLogic.current-mods[mod-index].dll-files; + selected(file) => { + if file != selected-dll { + if selected-order > 0 { + MainLogic.modify-order(self.current-value, selected-dll, selected-order - 1) + } + MainLogic.current-mods[mod-index].order.dll = file + } + } + } + SpinBox { + width: 106px; + enabled: load-order.checked; + minimum: 1; + // MARK: TODO + // max needs to be num of current-mods.order.set + maximum: MainLogic.current-mods.length; + value: selected-order; + edited(int) => { + if selected-dll != "" && int > 0 { + MainLogic.modify-order(selected-dll, selected-dll, int - 1) + } + MainLogic.current-mods[mod-index].order.at = int + } + } + } + } edit-mod-box := GroupBox { - title: @tr("Edit Mod"); - width: Formatting.group-box-width; - height: 90px; + title: @tr("Mod Actions"); + height: Formatting.group-box-r1-height; HorizontalLayout { - width: Formatting.group-box-width; - padding-right: Formatting.side-padding; - padding-bottom: Formatting.side-padding / 2; - alignment: button-layout; spacing: Formatting.button-spacing; + alignment: button-layout; Button { width: button-width; - height: 35px; + height: Formatting.default-element-height; primary: !SettingsLogic.dark-mode; text: @tr("Add Files"); clicked => { MainLogic.add-to-mod(MainLogic.current-mods[mod-index].name) } } - if (has-config) : Button { + if has-config : Button { width: button-width; - height: 35px; + height: Formatting.default-element-height; primary: !SettingsLogic.dark-mode; text: @tr("Edit config"); clicked => { MainLogic.edit-config(MainLogic.current-mods[mod-index].config-files) } } Button { width: button-width; - height: 35px; + height: Formatting.default-element-height; primary: !SettingsLogic.dark-mode; text: @tr("De-register"); clicked => { MainLogic.remove-mod(MainLogic.current-mods[mod-index].name) } From 29860535cb78783a1ab557d26aa0add421684a16 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 28 Apr 2024 15:45:49 -0500 Subject: [PATCH 24/62] rename \test_files\ to \temp" --- .gitignore | 2 +- benches/data_collection_benchmark.rs | 2 +- tests/test_ini_tools.rs | 6 +++--- tests/test_lib.rs | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index f76e85f..7b56e90 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ # Generated by Cargo # will have compiled files and executables /target/ -/test_files/ +/temp/ /.vscode/ /EML_gui_config.ini diff --git a/benches/data_collection_benchmark.rs b/benches/data_collection_benchmark.rs index 8a46dc6..ec64364 100644 --- a/benches/data_collection_benchmark.rs +++ b/benches/data_collection_benchmark.rs @@ -7,7 +7,7 @@ use std::{ path::{Path, PathBuf}, }; -const BENCH_TEST_FILE: &str = "test_files\\benchmark_test.ini"; +const BENCH_TEST_FILE: &str = "temp\\benchmark_test.ini"; const NUM_ENTRIES: u32 = 25; fn populate_non_valid_ini(len: u32, file: &Path) { diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 501947c..2d7c285 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -16,7 +16,7 @@ mod tests { #[test] fn does_u32_parse() { let test_nums: [u32; 3] = [2342652342, 2343523423, 69420]; - let test_file = Path::new("test_files\\test_nums.ini"); + let test_file = Path::new("temp\\test_nums.ini"); new_cfg(test_file).unwrap(); for (i, num) in test_nums.iter().enumerate() { @@ -48,7 +48,7 @@ mod tests { let test_path_1 = Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); let test_path_2 = Path::new("C:\\Windows\\System32"); - let test_file = Path::new("test_files\\test_path.ini"); + let test_file = Path::new("temp\\test_path.ini"); { new_cfg(test_file).unwrap(); @@ -73,7 +73,7 @@ mod tests { #[test] fn read_write_delete_from_ini() { - let test_file = Path::new("test_files\\test_collect_mod_data.ini"); + let test_file = Path::new("temp\\test_collect_mod_data.ini"); let mod_1_key = "Unlock The Fps "; let mod_1_state = false; let mod_2_key = "Skip The Intro"; diff --git a/tests/test_lib.rs b/tests/test_lib.rs index 77216b1..b52fe9f 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -22,16 +22,16 @@ mod tests { let dir_to_test_files = Path::new("C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui"); - let save_file = Path::new("test_files\\file_toggle_test.ini"); + let save_file = Path::new("temp\\file_toggle_test.ini"); new_cfg(save_file).unwrap(); let test_files = vec![ - PathBuf::from("test_files\\test1.txt"), - PathBuf::from("test_files\\test2.bhd"), - PathBuf::from("test_files\\test3.dll"), - PathBuf::from("test_files\\test4.exe"), - PathBuf::from("test_files\\test5.bin"), - PathBuf::from("test_files\\config.ini"), + PathBuf::from("temp\\test1.txt"), + PathBuf::from("temp\\test2.bhd"), + PathBuf::from("temp\\test3.dll"), + PathBuf::from("temp\\test4.exe"), + PathBuf::from("temp\\test5.bin"), + PathBuf::from("temp\\config.ini"), ]; let test_mod = RegMod::new("Test", true, test_files.clone()); From c61b492e7df742585efd6a225d2783198db7856e Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 29 Apr 2024 20:33:12 -0500 Subject: [PATCH 25/62] inital deserialize of load_order data work needs to be done on deserialization still, most will get moved into RegMod in parser.rs added ability to parse "0" and "1" to bool added returns for UI functions that can error, This way the front end can update the state cleanly if by checking if the called function failed or not fixed bug that show_terminal state was never updated on app startup --- src/main.rs | 229 +++++++++++++++++++++++++--------------- src/utils/ini/parser.rs | 10 +- 2 files changed, 152 insertions(+), 87 deletions(-) diff --git a/src/main.rs b/src/main.rs index c38b963..3b312b7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ use tokio::sync::{ slint::include_modules!(); const CONFIG_NAME: &str = "EML_gui_config.ini"; +const DEFAULT_VALUES: [&str; 2] = ["5000", "0"]; static GLOBAL_NUM_KEY: AtomicU32 = AtomicU32::new(0); static RESTRICTED_FILES: OnceLock<[&'static OsStr; 6]> = OnceLock::new(); static RECEIVER: OnceLock>> = OnceLock::new(); @@ -130,12 +131,12 @@ fn main() -> Result<(), slint::PlatformError> { .into(), ); let _ = get_or_update_game_dir(Some(game_dir.clone().unwrap_or_default())); - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, !game_verified).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] - }), - )); + }), ui.as_weak() + ); let mod_loader: ModLoader; if !game_verified { ui.global::().set_current_subpage(1); @@ -152,30 +153,30 @@ fn main() -> Result<(), slint::PlatformError> { .set_loader_disabled(mod_loader.disabled); if mod_loader.installed { ui.global::().set_loader_installed(true); - if let Ok(mod_loader_ini) = get_cfg(&mod_loader.cfg) { - match IniProperty::::read( - &mod_loader_ini, - LOADER_SECTIONS[0], - LOADER_KEYS[0], - false, - ) { - Some(delay_time) => ui - .global::() - .set_load_delay(SharedString::from(format!("{}ms", delay_time.value))), - None => { - error!("Found an unexpected character saved in \"load_delay\" Reseting to default value"); - save_value_ext( - &mod_loader.cfg, - LOADER_SECTIONS[0], - LOADER_KEYS[0], - "5000", - ) - .unwrap_or_else(|err| error!("{err}")); - } - } - } else { - error!("Error: could not read \"mod_loader_config.ini\""); - } + let loader_cfg = ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[0]).unwrap(); + let delay = loader_cfg.get_load_delay().unwrap_or_else(|err| { + let err = format!("{err} Reseting to default value"); + error!("{err}"); + save_value_ext(&mod_loader.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_VALUES[0]) + .unwrap_or_else(|err| error!("{err}")); + DEFAULT_VALUES[0].parse().unwrap() + }); + let show_terminal = loader_cfg.get_show_terminal().unwrap_or_else(|err| { + let err = format!("{err} Reseting to default value"); + error!("{err}"); + save_value_ext(&mod_loader.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_VALUES[1]) + .unwrap_or_else(|err| error!("{err}")); + false + }); + + ui.global::().set_load_delay(SharedString::from(format!("{}ms", delay))); + ui.global::().set_show_terminal(show_terminal); + + // Some(delay_time) => + // None => { + // error!("Found an unexpected character saved in \"load_delay\" Reseting to default value"); + // + // } } if !first_startup && !mod_loader.installed { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", &game_dir.display())); @@ -194,12 +195,12 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); match confirm_scan_mods(ui.as_weak(), &get_or_update_game_dir(None), current_ini, false).await { Ok(len) => { - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, false).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] - }), - )); + }), ui.as_weak() + ); ui.display_msg(&format!("Successfully Found {len} mod(s)")); let _ = receive_msg().await; } @@ -334,7 +335,7 @@ fn main() -> Result<(), slint::PlatformError> { }); ui.global::() .set_line_edit_text(SharedString::new()); - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors match RegMod::collect(current_ini, false) { @@ -344,8 +345,8 @@ fn main() -> Result<(), slint::PlatformError> { vec![RegMod::default()] } } - }), - )); + }),ui.as_weak() + ); }).unwrap(); } }); @@ -426,7 +427,7 @@ fn main() -> Result<(), slint::PlatformError> { }); ui.global::().on_toggle_mod({ let ui_handle = ui.as_weak(); - move |key, state| { + move |key, state| -> bool { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); let game_dir = get_or_update_game_dir(None); @@ -438,10 +439,9 @@ fn main() -> Result<(), slint::PlatformError> { { let result = toggle_files(&game_dir, state, found_mod, Some(current_ini)); if result.is_ok() { - return; + return state; } - let err = result.unwrap_err(); - ui.display_msg(&err.to_string()); + ui.display_msg(&result.unwrap_err().to_string()); } else { error!("Mod: \"{key}\" not found"); ui.display_msg(&format!("Mod: \"{key}\" not found")) @@ -449,8 +449,7 @@ fn main() -> Result<(), slint::PlatformError> { } Err(err) => ui.display_msg(&err.to_string()), } - ui.global::().set_if_err_bool(!state); - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors match RegMod::collect(current_ini, false) { @@ -460,8 +459,9 @@ fn main() -> Result<(), slint::PlatformError> { vec![RegMod::default()] } } - }), - )); + }), ui.as_weak() + ); + !state } }); ui.global::().on_force_app_focus({ @@ -579,7 +579,7 @@ fn main() -> Result<(), slint::PlatformError> { if !results.iter().any(|r| r.is_err()) { ui.display_msg(&format!("Sucessfully added {} file(s) to {}", num_files, format_key)); } - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, false).unwrap_or_else(|_| { match RegMod::collect(current_ini, false) { Ok(mods) => mods, @@ -588,8 +588,8 @@ fn main() -> Result<(), slint::PlatformError> { vec![RegMod::default()] } } - }), - )); + }),ui.as_weak() + ); } } else { error!("Mod: \"{key}\" not found"); @@ -653,7 +653,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&format!("{err}\nRemoving invalid entries")) }; ui.global::().set_current_subpage(0); - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, false).unwrap_or_else(|_| { match RegMod::collect(current_ini, false) { Ok(mods) => mods, @@ -662,8 +662,8 @@ fn main() -> Result<(), slint::PlatformError> { vec![RegMod::default()] } } - }), - )); + }),ui.as_weak() + ); }).unwrap(); } }); @@ -708,16 +708,18 @@ fn main() -> Result<(), slint::PlatformError> { }); ui.global::().on_toggle_terminal({ let ui_handle = ui.as_weak(); - move |state| { + move |state| -> bool { let ui = ui_handle.unwrap(); let value = if state { "1" } else { "0" }; let ext_ini = get_or_update_game_dir(None).join(LOADER_FILES[0]); + let mut result = state; save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( |err| { ui.display_msg(&err.to_string()); - ui.global::().set_show_terminal(!state); + result = !state; }, ); + result } }); ui.global::().on_set_load_delay({ @@ -738,7 +740,7 @@ fn main() -> Result<(), slint::PlatformError> { }); ui.global::().on_toggle_all({ let ui_handle = ui.as_weak(); - move |state| { + move |state| -> bool { let ui = ui_handle.unwrap(); let game_dir = get_or_update_game_dir(None); let files = if state { @@ -748,10 +750,10 @@ fn main() -> Result<(), slint::PlatformError> { }; let main_dll = RegMod::new("main", !state, files); match toggle_files(&game_dir, !state, &main_dll, None) { - Ok(_) => ui.global::().set_loader_disabled(state), + Ok(_) => state, Err(err) => { ui.display_msg(&format!("{err}")); - ui.global::().set_loader_disabled(!state) + !state } } } @@ -793,17 +795,16 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); slint::spawn_local(async move { - let ui_handle = ui.as_weak(); let game_dir = get_or_update_game_dir(None); - match confirm_scan_mods(ui_handle, &game_dir, current_ini, true).await { + match confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); - ui.global::().set_current_mods(deserialize( + deserialize_current_mods( &RegMod::collect(current_ini, false).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] - }), - )); + }),ui.as_weak() + ); ui.display_msg(&format!("Successfully Found {len} mod(s)")); } Err(err) => if err.kind() != ErrorKind::ConnectionAborted { @@ -815,14 +816,16 @@ fn main() -> Result<(), slint::PlatformError> { }); ui.global::().on_add_remove_order({ let ui_handle = ui.as_weak(); - move |state, key, value| { + move |state, key, value| -> i32 { let ui = ui_handle.unwrap(); + let error = 42069_i32; let game_dir = get_or_update_game_dir(None); + let mut result: i32 = if state { 1 } else { -1 }; let mut load_order = match ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err); - return; + return error; } }; let load_orders = load_order.mut_section(); @@ -833,7 +836,7 @@ fn main() -> Result<(), slint::PlatformError> { } false => { if !load_orders.contains_key(&key) { - return; + return error; } load_orders.remove(&key); None @@ -841,52 +844,64 @@ fn main() -> Result<(), slint::PlatformError> { }; update_order_entries(stable_k, load_orders).unwrap_or_else(|err| { ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); + result = error; std::mem::swap(load_orders, &mut ini::Properties::new()); }); - // ui.global::().set_current_mods(deserialize( - // &RegMod::collect(&get_ini_dir(), false).unwrap_or_else(|err| { - // ui.display_msg(&err.to_string()); - // vec![RegMod::default()] - // }), - // )); load_order.write_to_file().unwrap_or_else(|err| { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); + result = error; }); + result } }); ui.global::().on_modify_order({ let ui_handle = ui.as_weak(); - move |to_k, from_k, value| { + move |to_k, from_k, value| -> i32 { let ui = ui_handle.unwrap(); + let mut result = 0_i32; let game_dir = get_or_update_game_dir(None); let mut load_order = match ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err); - return; + return -1; } }; let load_orders = load_order.mut_section(); - if to_k != from_k { + if to_k != from_k && load_orders.contains_key(&from_k) { load_orders.remove(from_k); - load_orders.append(&to_k, value.to_string()); - } else { + load_orders.append(&to_k, value.to_string()) + } else if load_orders.contains_key(&to_k) { load_orders.insert(&to_k, value.to_string()) + } else { + load_orders.append(&to_k, value.to_string()); + result = 1 }; + // MARK: TODO + // we need to call for a full deserialize if one of these functions error update_order_entries(Some(&to_k), load_orders).unwrap_or_else(|err| { ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); + // reseting the load order count here is not enough + result = -1; std::mem::swap(load_orders, &mut ini::Properties::new()); }); - // ui.global::().set_current_mods(deserialize( - // &RegMod::collect(&get_ini_dir(), false).unwrap_or_else(|err| { - // ui.display_msg(&err.to_string()); - // vec![RegMod::default()] - // }), - // )); load_order.write_to_file().unwrap_or_else(|err| { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); + if !result.is_negative() { result = -1 } }); + result + } + }); + ui.global::().on_force_deserialize({ + let ui_handle = ui.as_weak(); + move || { + let ui = ui_handle.unwrap(); + deserialize_current_mods(&RegMod::collect(get_ini_dir(), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }), ui.as_weak()); + info!("deserialized after encountered error"); } }); @@ -1020,15 +1035,54 @@ fn open_text_files(ui_handle: slint::Weak, files: Vec) } } -fn deserialize(data: &[RegMod]) -> ModelRc { +// fn save_and_update_order_data(new: &mut ModLoaderCfg, ui_handle: slint::Weak) { +// let ui = ui_handle.unwrap(); +// new.write_to_file().unwrap_or_else(|err| { +// ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); +// std::mem::take(new); +// }); +// // if this case change bounds to grab cached data instead of read +// let reg_mods = match new.is_empty() { +// true => +// RegMod::collect(get_ini_dir(), false).unwrap_or_else(|err| { +// ui.display_msg(&err.to_string()); +// vec![RegMod::default()] +// }), +// false => vec![RegMod::default()], +// }; +// deserialize_current_mods( +// ®_mods,Some(new), ui.as_weak() +// ); +// } + +fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { + let ui = ui_handle.unwrap(); + let mut load_order_parsed = ModLoaderCfg::load(&get_or_update_game_dir(None), LOADER_SECTIONS[1]) + .unwrap_or_default() + .parse() + .unwrap_or_default(); + + let mut has_order_count = 0; let display_mods: Rc> = Default::default(); - for mod_data in data.iter() { + for mod_data in mods.iter() { let files: Rc> = Default::default(); let dll_files: Rc> = Default::default(); let config_files: Rc> = Default::default(); + let mut order_set = false; + let mut order_v = 0_usize; + let mut order_i = 0_usize; if !mod_data.mod_files.is_empty() { files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); - dll_files.extend(mod_data.mod_files.iter().map(|f|SharedString::from(f.file_name().unwrap().to_string_lossy().to_string()))); + dll_files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.file_name().unwrap().to_string_lossy().replace(OFF_STATE, "")))); + for (i, dll) in dll_files.iter().enumerate() { + if let Some(remove_i) = load_order_parsed.iter().position(|(k, _)| *k == *dll) { + order_set = true; + order_i = i; + order_v = load_order_parsed.swap_remove(remove_i).1; + has_order_count += 1; + break; + } + }; }; if !mod_data.config_files.is_empty() { files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); @@ -1050,15 +1104,22 @@ fn deserialize(data: &[RegMod]) -> ModelRc { config_files: ModelRc::from(config_files), dll_files: ModelRc::from(dll_files), // MARK: TODO - // parse out LoadOrder properties // need to be able to sort RegMods by load-order then albethabetical // need a counter for entries in Load-order or order.set == true - // at - front end is 1 index | if set false LoadOrder::default() - order: LoadOrder::default(), + // `order.at` in the front end is 1 index | back end is 0 index + order: if order_set { LoadOrder { + set: order_set, + i: order_i as i32, + at: order_v as i32 + 1, + }} else { LoadOrder::default() }, }) } - ModelRc::from(display_mods) + ui.global::().set_current_mods(ModelRc::from(display_mods)); + ui.global::().set_orders_set(has_order_count); } +// MARK: TODO +// need to use ModelNotify::row_changed to handle updating page info on change +// ui.invoke_update_mod_index(1, 1); async fn install_new_mod( name: &str, diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index d60eed1..c23807c 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -40,10 +40,14 @@ impl ValueType for bool { key: &str, _skip_validation: bool, ) -> Result { - ini.get_from(section, key) + match ini + .get_from(section, key) .expect("Validated by IniProperty::is_valid") - .to_lowercase() - .parse::() + { + "0" => Ok(false), + "1" => Ok(true), + c => c.to_lowercase().parse::(), + } } } From 0ade3b7efe3a818e51ff1300e15634f18f610c3a Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 29 Apr 2024 20:38:44 -0500 Subject: [PATCH 26/62] impl general functions for ModLoaderCfg added parse::>(), get_load_delay(), get_show_terminal() --- src/utils/ini/mod_loader.rs | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index 7ae5b11..d6050d1 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -3,7 +3,9 @@ use log::{error, info, warn}; use std::path::{Path, PathBuf}; use crate::{ + utils::ini::parser::IniProperty, utils::ini::writer::EXT_OPTIONS, + LOADER_KEYS, LOADER_SECTIONS, {does_dir_contain, get_cfg, Operation, LOADER_FILES, LOADER_FILES_DISABLED}, }; @@ -53,6 +55,7 @@ impl ModLoader { } } +#[derive(Default)] pub struct ModLoaderCfg { cfg: Ini, cfg_dir: PathBuf, @@ -93,10 +96,51 @@ impl ModLoaderCfg { }) } + pub fn get_load_delay(&self) -> Result { + match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], false) { + Some(delay_time) => Ok(delay_time.value), + None => Err(format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[0] + )), + } + } + + pub fn get_show_terminal(&self) -> Result { + match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1], false) { + Some(delay_time) => Ok(delay_time.value), + None => Err(format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[0] + )), + } + } + pub fn mut_section(&mut self) -> &mut ini::Properties { self.cfg.section_mut(self.section.as_ref()).unwrap() } + fn section(&self) -> &ini::Properties { + self.cfg.section(self.section.as_ref()).unwrap() + } + + fn iter(&self) -> ini::PropertyIter { + self.section().iter() + } + + pub fn parse(&self) -> Result, std::num::ParseIntError> { + self.iter() + .map(|(k, v)| { + let parse_v = v.parse::(); + Ok((k.to_string(), parse_v?)) + }) + .collect::, _>>() + } + + pub fn is_empty(&self) -> bool { + self.section().is_empty() + } + pub fn dir(&self) -> &Path { &self.cfg_dir } From 6ee6fb9ac9cb347c2792a566fcc69252718022b4 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 29 Apr 2024 20:42:57 -0500 Subject: [PATCH 27/62] Error checking callbacks added callback for being able to set the index of current-mods and tab of the edit-mod-page --- ui/appwindow.slint | 7 +++++-- ui/common.slint | 15 ++++++++------- ui/main.slint | 20 ++++++++++---------- ui/settings.slint | 14 ++++++++++++-- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/ui/appwindow.slint b/ui/appwindow.slint index c657ce2..04554b9 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -40,11 +40,12 @@ export component App inherits Window { min-width: Formatting.app-width; max-width: Formatting.app-width; - mp := MainPage {} - callback focus-app; callback show-error-popup; callback show-confirm-popup; + callback update-mod-index(int, int); + + update-mod-index(i, t) => { mp.update-mod-index(i, t) } focus-app => { fs.focus() } show-error-popup => { popup-visible = true; @@ -54,6 +55,8 @@ export component App inherits Window { popup-visible = true; alt-std-buttons ? confirm-popup-2.show() : confirm-popup.show() } + + mp := MainPage {} msg-size := Text { visible: false; diff --git a/ui/common.slint b/ui/common.slint index 42b87c2..70e70c5 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -1,6 +1,6 @@ struct LoadOrder { set: bool, - dll: string, + i: int, at: int, } @@ -17,20 +17,21 @@ export struct DisplayMod { export enum Message { confirm, deny, esc } export global MainLogic { - callback toggle-mod(string, bool); + callback toggle-mod(string, bool) -> bool; callback select-mod-files(string); callback add-to-mod(string); callback remove-mod(string); callback edit-config([string]); callback edit-config-item(StandardListViewItem); // consider using a string instead of an int for value - callback add-remove-order(bool, string, int); - callback modify-order(string, string, int); + callback add-remove-order(bool, string, int) -> int; + callback modify-order(string, string, int) -> int; callback force-app-focus(); + callback force-deserialize(); callback send-message(Message); in property line-edit-text; - in-out property if-err-bool; in-out property game-path-valid; + in-out property orders-set; in-out property current-subpage: 0; in-out property <[DisplayMod]> current-mods: [ {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, order: {set: false}}, @@ -42,9 +43,9 @@ export global SettingsLogic { callback open-game-dir(); callback scan-for-mods(); callback toggle-theme(bool); - callback toggle-terminal(bool); + callback toggle-terminal(bool) -> bool; callback set-load-delay(string); - callback toggle-all(bool); + callback toggle-all(bool) -> bool; in property game-path: "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; in property loader-installed; in-out property dark-mode: true; diff --git a/ui/main.slint b/ui/main.slint index a01bea0..2644fdd 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -13,12 +13,15 @@ export component MainPage inherits Page { callback focus-line-edit; callback focus-settings; callback swap-tab; - callback edit-mod(int); + callback edit-mod(int, int); + callback update-mod-index(int, int); focus-line-edit => { input-mod.focus() } focus-settings => { app-settings.focus-settings-scope() } swap-tab => { mod-settings.current-tab = mod-settings.current-tab == 0 ? 1 : 0 } - edit-mod(i) => { - mod-settings.current-tab = 0; + + update-mod-index(i, t) => { edit-mod(i, t) } + edit-mod(i, t) => { + mod-settings.current-tab = t; mod-settings.mod-index = i; MainLogic.current-subpage = 2 } @@ -45,12 +48,9 @@ export component MainPage inherits Page { checked: mod.enabled; enabled: reg-mod-box.enabled; toggled => { - MainLogic.if-err-bool = self.checked; - MainLogic.current-mods[idx].enabled = self.checked; - MainLogic.toggle-mod(mod.name, self.checked); - if (MainLogic.if-err-bool != self.checked) { - self.checked = MainLogic.if-err-bool; - MainLogic.current-mods[idx].enabled = MainLogic.if-err-bool; + MainLogic.current-mods[idx].enabled = MainLogic.toggle-mod(mod.name, self.checked); + if MainLogic.current-mods[idx].enabled != self.checked { + self.checked = !self.checked; } } } @@ -66,7 +66,7 @@ export component MainPage inherits Page { x: mod-box.width - 284px; height: 28px; width: root.width - mod-box.width; - clicked => { edit-mod(idx) } + clicked => { edit-mod(idx, 0) } } } states [ diff --git a/ui/settings.slint b/ui/settings.slint index 004feee..3490bc8 100644 --- a/ui/settings.slint +++ b/ui/settings.slint @@ -105,7 +105,12 @@ export component SettingsPage inherits Page { text: @tr("Show Terminal"); enabled: SettingsLogic.loader-installed; checked <=> SettingsLogic.show-terminal; - toggled => { SettingsLogic.toggle-terminal(self.checked) } + toggled => { + SettingsLogic.show-terminal = SettingsLogic.toggle-terminal(self.checked); + if SettingsLogic.show-terminal != self.checked { + self.checked = !self.checked; + } + } } } HorizontalLayout { @@ -116,7 +121,12 @@ export component SettingsPage inherits Page { text: @tr("Disable All mods"); enabled: SettingsLogic.loader-installed; checked <=> SettingsLogic.loader-disabled; - toggled => { SettingsLogic.toggle-all(self.checked) } + toggled => { + SettingsLogic.loader-disabled = SettingsLogic.toggle-all(self.checked); + if SettingsLogic.loader-disabled != self.checked { + self.checked = !self.checked; + } + } } } HorizontalLayout { From eef3833a2192f50b1178e1d6899b77b582e6fc56 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 29 Apr 2024 20:45:42 -0500 Subject: [PATCH 28/62] Front end will handle mod_load_order state Front end will entirely handle the state of mod_load_order since it's not too complicated and slint can handle this logic. The back end will step in and try and correct state when a function callback encounters an error. --- ui/tabs.slint | 153 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 97 insertions(+), 56 deletions(-) diff --git a/ui/tabs.slint b/ui/tabs.slint index 06d0685..e5f73c8 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -42,32 +42,12 @@ export component ModDetails inherits Tab { } } -// component ChangeObserver { -// in property i; -// in property value: MainLogic.current-mods[i].order.set; -// // first argument is new value; must return second argument -// pure callback changed(bool, float) -> float; - -// width: 0; height: 0; visible: false; - -// opacity: changed(value, 1); -// } - -// Text { -// property debug: MainLogic.current-mods[mod-index].order.set ? "true" : "false"; -// text: debug; -// } - -// Button { -// text: "test"; -// clicked => {MainLogic.current-mods[mod-index].order.set = !MainLogic.current-mods[mod-index].order.set} -// } - export component ModEdit inherits Tab { in property mod-index; property has-config: MainLogic.current-mods[mod-index].config-files.length > 0; property selected-order: MainLogic.current-mods[mod-index].order.at; - property selected-dll: MainLogic.current-mods[mod-index].order.dll; + property selected-index: MainLogic.current-mods[mod-index].order.i; + property selected-dll: MainLogic.current-mods[mod-index].dll-files[selected-index]; property button-width: has-config ? 96px : 105px; property button-layout: has-config ? space-between : end; VerticalLayout { @@ -76,25 +56,62 @@ export component ModEdit inherits Tab { padding-bottom: Formatting.side-padding / 2; alignment: space-between; - GroupBox { + // MARK: TODO + // inbed load_order data inside RegMod::collect for init deserialize + // need to use ModelNotify::row_changed to handle updating page info on change + load-order-box := GroupBox { + property temp; title: @tr("Load Order"); + enabled: MainLogic.current-mods[mod-index].dll-files.length > 0; + + function init-selected-index() { + if MainLogic.current-mods[mod-index].dll-files.length != 1 { + MainLogic.current-mods[mod-index].order.i = -1; + } + } + + init => { + if !MainLogic.current-mods[mod-index].order.set { + init-selected-index() + } + } HorizontalLayout { row: 1; padding-top: Formatting.default-padding; load-order := Switch { text: @tr("Set Load Order"); - enabled: MainLogic.current-mods[mod-index].dll-files.length > 0; - // A two way binding on checked would be nice, this would clean up the code and ? keep state in sync ? + enabled: load-order-box.enabled; checked: MainLogic.current-mods[mod-index].order.set; toggled => { - if MainLogic.current-mods[mod-index].dll-files.length == 1 && selected-dll == "" { - MainLogic.current-mods[mod-index].order.dll = MainLogic.current-mods[mod-index].dll-files[dll-selection.current-index]; - } - if MainLogic.current-mods[mod-index].order.dll != "" && selected-order > 0 { - // Front end is 1 based and back end is 0 based - MainLogic.add-remove-order(self.checked, selected-dll, selected-order - 1) + if self.checked { + MainLogic.current-mods[mod-index].order.at = MainLogic.orders-set + 1; + if selected-index != -1 && selected-order > 0 { + // Front end `order.at` is 1 based and back end is 0 based + temp = MainLogic.add-remove-order(self.checked, selected-dll, selected-order - 1); + if temp != 42069 { + MainLogic.orders-set = MainLogic.orders-set + temp; + MainLogic.current-mods[mod-index].order.set = self.checked; + } else { + MainLogic.force-deserialize() + } + temp = 0 + } + } else { + if selected-index != -1 && selected-order > 0 { + temp = MainLogic.add-remove-order(self.checked, selected-dll, selected-order - 1); + if temp != 42069 { + MainLogic.orders-set = MainLogic.orders-set + temp; + init-selected-index() + } else { + MainLogic.force-deserialize(); + } + } + if temp != 42069 { + MainLogic.current-mods[mod-index].order.set = self.checked; + MainLogic.current-mods[mod-index].order.at = 0; + } + temp = 0 } - MainLogic.current-mods[mod-index].order.set = self.checked } } } @@ -102,38 +119,62 @@ export component ModEdit inherits Tab { row: 2; padding-top: Formatting.side-padding; spacing: Formatting.default-spacing; - dll-selection := ComboBox { - enabled: load-order.checked; - // MARK: TODO - // need to find a way to have current value selected if already enabled - // combo-box is weird if you use current value with a string outside of the model - // sort dll files by name and then store index? - // current-value: selected-dll; - current-index: MainLogic.current-mods[mod-index].dll-files.length == 1 ? 0 : -1; - model: MainLogic.current-mods[mod-index].dll-files; - selected(file) => { - if file != selected-dll { - if selected-order > 0 { - MainLogic.modify-order(self.current-value, selected-dll, selected-order - 1) + + function modify-file(file: string, i: int) { + if file != selected-dll && selected-order > 0 { + temp = MainLogic.modify-order(file, selected-dll, selected-order - 1); + if temp != -1 { + MainLogic.orders-set = MainLogic.orders-set + temp; + MainLogic.current-mods[mod-index].order.i = i; + if temp == 1 { + MainLogic.current-mods[mod-index].order.set = true; } - MainLogic.current-mods[mod-index].order.dll = file } + temp = 0 } + MainLogic.force-app-focus() } - SpinBox { + function modify-index(v: int) { + if selected-index != -1 && v > 0 { + temp = MainLogic.modify-order(selected-dll, selected-dll, v - 1); + if temp != -1 { + MainLogic.orders-set = MainLogic.orders-set + temp + } + } + if temp != -1 { + MainLogic.current-mods[mod-index].order.at = v + } + temp = 0 + } + + // Might be able to remove this hack after properly having sorting data parsed + if load-order.checked : ComboBox { + enabled: load-order.checked; + current-index: selected-index; + model: MainLogic.current-mods[mod-index].dll-files; + selected(file) => { modify-file(file, self.current-index) } + } + if !load-order.checked : ComboBox { + enabled: load-order.checked; + current-index: selected-index; + model: MainLogic.current-mods[mod-index].dll-files; + selected(file) => { modify-file(file, self.current-index) } + } + if load-order.checked : SpinBox { width: 106px; enabled: load-order.checked; minimum: 1; - // MARK: TODO - // max needs to be num of current-mods.order.set - maximum: MainLogic.current-mods.length; + maximum: MainLogic.orders-set; value: selected-order; - edited(int) => { - if selected-dll != "" && int > 0 { - MainLogic.modify-order(selected-dll, selected-dll, int - 1) - } - MainLogic.current-mods[mod-index].order.at = int - } + edited(int) => { modify-index(int) } + } + if !load-order.checked : SpinBox { + width: 106px; + enabled: load-order.checked; + minimum: 1; + maximum: MainLogic.orders-set; + value: selected-order; + edited(int) => { modify-index(int) } } } } From f4653b3c04019db0f1f995dd40f3adaee7377bea Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Mon, 29 Apr 2024 20:47:33 -0500 Subject: [PATCH 29/62] Cargo update --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9686105..efa668b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1640,9 +1640,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.29" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7" +checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", "miniz_oxide", @@ -2012,9 +2012,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -2560,9 +2560,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" [[package]] name = "libloading" From 0c83ad3151a5365b87e7502a5bd830aeada98396 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 30 Apr 2024 00:18:40 -0500 Subject: [PATCH 30/62] moved parsing LoadOrder inside of RegMod parsing added documentation for RegMod other small changes --- src/main.rs | 81 ++++---------------- src/utils/ini/mod_loader.rs | 12 ++- src/utils/ini/parser.rs | 148 ++++++++++++++++++++++++++++-------- tests/test_lib.rs | 12 +-- ui/common.slint | 5 +- ui/tabs.slint | 11 ++- 6 files changed, 158 insertions(+), 111 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3b312b7..0b8d2f6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use elden_mod_loader_gui::{ utils::{ ini::{ - mod_loader::{ModLoader, ModLoaderCfg, update_order_entries}, + mod_loader::{ModLoader, ModLoaderCfg, update_order_entries, Countable}, parser::{file_registered, IniProperty, RegMod, Valitidity}, writer::*, }, @@ -231,18 +231,16 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&results[0].as_ref().unwrap_err().to_string()); return; } + if registered_mods + .iter() + .any(|mod_data| format_key.to_lowercase() == mod_data.name.to_lowercase()) { - if registered_mods - .iter() - .any(|mod_data| format_key.to_lowercase() == mod_data.name.to_lowercase()) - { - ui.display_msg(&format!( - "There is already a registered mod with the name\n\"{mod_name}\"" - )); - ui.global::() - .set_line_edit_text(SharedString::new()); - return; - } + ui.display_msg(&format!( + "There is already a registered mod with the name\n\"{mod_name}\"" + )); + ui.global::() + .set_line_edit_text(SharedString::new()); + return; } slint::spawn_local(async move { let game_dir = get_or_update_game_dir(None); @@ -831,7 +829,7 @@ fn main() -> Result<(), slint::PlatformError> { let load_orders = load_order.mut_section(); let stable_k = match state { true => { - load_orders.insert(&key, &value.to_string()); + load_orders.insert(&key, &value); Some(key.as_str()) } false => { @@ -870,19 +868,16 @@ fn main() -> Result<(), slint::PlatformError> { let load_orders = load_order.mut_section(); if to_k != from_k && load_orders.contains_key(&from_k) { load_orders.remove(from_k); - load_orders.append(&to_k, value.to_string()) + load_orders.append(&to_k, value) } else if load_orders.contains_key(&to_k) { - load_orders.insert(&to_k, value.to_string()) + load_orders.insert(&to_k, value) } else { - load_orders.append(&to_k, value.to_string()); + load_orders.append(&to_k, value); result = 1 }; - // MARK: TODO - // we need to call for a full deserialize if one of these functions error update_order_entries(Some(&to_k), load_orders).unwrap_or_else(|err| { ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); - // reseting the load order count here is not enough result = -1; std::mem::swap(load_orders, &mut ini::Properties::new()); }); @@ -1035,54 +1030,16 @@ fn open_text_files(ui_handle: slint::Weak, files: Vec) } } -// fn save_and_update_order_data(new: &mut ModLoaderCfg, ui_handle: slint::Weak) { -// let ui = ui_handle.unwrap(); -// new.write_to_file().unwrap_or_else(|err| { -// ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); -// std::mem::take(new); -// }); -// // if this case change bounds to grab cached data instead of read -// let reg_mods = match new.is_empty() { -// true => -// RegMod::collect(get_ini_dir(), false).unwrap_or_else(|err| { -// ui.display_msg(&err.to_string()); -// vec![RegMod::default()] -// }), -// false => vec![RegMod::default()], -// }; -// deserialize_current_mods( -// ®_mods,Some(new), ui.as_weak() -// ); -// } - fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { let ui = ui_handle.unwrap(); - let mut load_order_parsed = ModLoaderCfg::load(&get_or_update_game_dir(None), LOADER_SECTIONS[1]) - .unwrap_or_default() - .parse() - .unwrap_or_default(); - - let mut has_order_count = 0; let display_mods: Rc> = Default::default(); for mod_data in mods.iter() { let files: Rc> = Default::default(); let dll_files: Rc> = Default::default(); let config_files: Rc> = Default::default(); - let mut order_set = false; - let mut order_v = 0_usize; - let mut order_i = 0_usize; if !mod_data.mod_files.is_empty() { files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); dll_files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.file_name().unwrap().to_string_lossy().replace(OFF_STATE, "")))); - for (i, dll) in dll_files.iter().enumerate() { - if let Some(remove_i) = load_order_parsed.iter().position(|(k, _)| *k == *dll) { - order_set = true; - order_i = i; - order_v = load_order_parsed.swap_remove(remove_i).1; - has_order_count += 1; - break; - } - }; }; if !mod_data.config_files.is_empty() { files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); @@ -1105,17 +1062,11 @@ fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { dll_files: ModelRc::from(dll_files), // MARK: TODO // need to be able to sort RegMods by load-order then albethabetical - // need a counter for entries in Load-order or order.set == true - // `order.at` in the front end is 1 index | back end is 0 index - order: if order_set { LoadOrder { - set: order_set, - i: order_i as i32, - at: order_v as i32 + 1, - }} else { LoadOrder::default() }, + order: LoadOrder { at: mod_data.order.at as i32 + 1, i: mod_data.order.i as i32, set: mod_data.order.set }, }) } ui.global::().set_current_mods(ModelRc::from(display_mods)); - ui.global::().set_orders_set(has_order_count); + ui.global::().set_orders_set(mods.order_count() as i32); } // MARK: TODO // need to use ModelNotify::row_changed to handle updating page info on change diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index d6050d1..d823432 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -3,7 +3,7 @@ use log::{error, info, warn}; use std::path::{Path, PathBuf}; use crate::{ - utils::ini::parser::IniProperty, + utils::ini::parser::{IniProperty, RegMod}, utils::ini::writer::EXT_OPTIONS, LOADER_KEYS, LOADER_SECTIONS, {does_dir_contain, get_cfg, Operation, LOADER_FILES, LOADER_FILES_DISABLED}, @@ -185,3 +185,13 @@ pub fn update_order_entries( } Ok(()) } + +pub trait Countable { + fn order_count(&self) -> usize; +} + +impl<'a> Countable for &'a [RegMod] { + fn order_count(&self) -> usize { + self.iter().filter(|m| m.order.set).count() + } +} diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index c23807c..fe1d332 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -9,8 +9,11 @@ use std::{ use crate::{ get_cfg, new_io_error, toggle_files, - utils::ini::writer::{remove_array, remove_entry, INI_SECTIONS}, - FileData, + utils::ini::{ + mod_loader::ModLoaderCfg, + writer::{remove_array, remove_entry, INI_SECTIONS}, + }, + FileData, LOADER_SECTIONS, OFF_STATE, }; pub trait ValueType: Sized { @@ -262,42 +265,107 @@ impl Valitidity for Ini { #[derive(Default)] pub struct RegMod { + /// Key in snake_case pub name: String, + + /// true = enabled | false = disabled pub state: bool, + + /// files with extension `.dll` | also possible they end in `.dll.disabled` + /// saved as short paths with `game_dir` truncated pub mod_files: Vec, + + /// files with extension `.ini` + /// saved as short paths with `game_dir` truncated pub config_files: Vec, + + /// files with any extension other than `.dll` or `.ini` + /// saved as short paths with `game_dir` truncated pub other_files: Vec, + + /// contains properties related to if a mod has a set load order + pub order: LoadOrder, +} + +#[derive(Default)] +pub struct LoadOrder { + /// if one of `self.mod_files` has a set load_order + pub set: bool, + + /// the index of the selected `.dll` within `self.mod_files` + pub i: usize, + + /// current set value of `load_order` + /// `self.order.at` is stored as 0 index | front end uses 1 index + pub at: usize, } impl RegMod { + fn split_out_config_files( + in_files: Vec, + ) -> (Vec, Vec, Vec) { + let len = in_files.len(); + let mut mod_files = Vec::with_capacity(len); + let mut config_files = Vec::with_capacity(len); + let mut other_files = Vec::with_capacity(len); + in_files.into_iter().for_each(|file| { + match FileData::from(&file.to_string_lossy()).extension { + ".dll" => mod_files.push(file), + ".ini" => config_files.push(file), + _ => other_files.push(file), + } + }); + (mod_files, config_files, other_files) + } + /// This function omits the population of the `order` field pub fn new(name: &str, state: bool, in_files: Vec) -> Self { - fn split_out_config_files( - in_files: Vec, - ) -> (Vec, Vec, Vec) { - let mut mod_files = Vec::with_capacity(in_files.len()); - let mut config_files = Vec::with_capacity(in_files.len()); - let mut other_files = Vec::with_capacity(in_files.len()); - in_files.into_iter().for_each(|file| { - match FileData::from(&file.to_string_lossy()).extension { - ".dll" => mod_files.push(file), - ".ini" => config_files.push(file), - _ => other_files.push(file), - } - }); - (mod_files, config_files, other_files) - } - let (mod_files, config_files, other_files) = split_out_config_files(in_files); + let (mod_files, config_files, other_files) = RegMod::split_out_config_files(in_files); RegMod { name: String::from(name), state, mod_files, config_files, other_files, + order: LoadOrder::default(), } } - pub fn collect(path: &Path, skip_validation: bool) -> std::io::Result> { + /// This function populates all fields + fn new_full( + name: &str, + state: bool, + in_files: Vec, + parsed_order_val: &mut Vec<(String, usize)>, + ) -> std::io::Result { + let (mod_files, config_files, other_files) = RegMod::split_out_config_files(in_files); + let mut order = LoadOrder::default(); + let dll_files = mod_files + .iter() + .map(|f| { + let file_name = f.file_name().ok_or(String::from("Bad file name")); + Ok(file_name?.to_string_lossy().replace(OFF_STATE, "")) + }) + .collect::, String>>() + .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?; + for (i, dll) in dll_files.iter().enumerate() { + if let Some(remove_i) = parsed_order_val.iter().position(|(k, _)| k == dll) { + order.set = true; + order.i = i; + order.at = parsed_order_val.swap_remove(remove_i).1; + break; + } + } + Ok(RegMod { + name: String::from(name), + state, + mod_files, + config_files, + other_files, + order, + }) + } + pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { type ModData<'a> = Vec<(&'a str, Result, Vec)>; - fn sync_keys<'a>(ini: &'a Ini, path: &Path) -> std::io::Result> { + fn sync_keys<'a>(ini: &'a Ini, ini_path: &Path) -> std::io::Result> { fn collect_file_data(section: &Properties) -> HashMap<&str, Vec<&str>> { section .iter() @@ -348,7 +416,7 @@ impl RegMod { .collect::>(); for key in invalid_state { state_data.remove(key); - remove_entry(path, Some("registered-mods"), key)?; + remove_entry(ini_path, Some("registered-mods"), key)?; warn!("\"{key}\" has no matching files"); } let invalid_files = file_data @@ -358,9 +426,9 @@ impl RegMod { .collect::>(); for key in invalid_files { if file_data.get(key).expect("key exists").len() > 1 { - remove_array(path, key)?; + remove_array(ini_path, key)?; } else { - remove_entry(path, Some("mod-files"), key)?; + remove_entry(ini_path, Some("mod-files"), key)?; } file_data.remove(key); warn!("\"{key}\" has no matching state"); @@ -390,7 +458,7 @@ impl RegMod { }) .collect() } - let ini = get_cfg(path).expect("Validated by Ini::is_setup on startup"); + let ini = get_cfg(ini_path)?; if skip_validation { let parsed_data = collect_data_unsafe(&ini); @@ -405,7 +473,17 @@ impl RegMod { }) .collect()) } else { - let parsed_data = sync_keys(&ini, path)?; + let parsed_data = sync_keys(&ini, ini_path)?; + let game_dir = IniProperty::::read(&ini, Some("paths"), "game_dir", false) + .ok_or(std::io::Error::new( + ErrorKind::InvalidData, + "Could not read \"game_dir\" from file", + ))? + .value; + let mut load_order_parsed = ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) + .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))? + .parse() + .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?; Ok(parsed_data .iter() .filter_map(|(k, s, f)| match &s { @@ -416,10 +494,13 @@ impl RegMod { .to_owned() .validate(&ini, Some("mod-files"), skip_validation) { - Ok(path) => Some(RegMod::new(k, *bool, vec![path])), + Ok(path) => { + RegMod::new_full(k, *bool, vec![path], &mut load_order_parsed) + .ok() + } Err(err) => { error!("Error: {err}"); - remove_entry(path, Some("registered-mods"), k) + remove_entry(ini_path, Some("registered-mods"), k) .expect("Key is valid"); None } @@ -429,10 +510,12 @@ impl RegMod { .to_owned() .validate(&ini, Some("mod-files"), skip_validation) { - Ok(paths) => Some(RegMod::new(k, *bool, paths)), + Ok(paths) => { + RegMod::new_full(k, *bool, paths, &mut load_order_parsed).ok() + } Err(err) => { error!("Error: {err}"); - remove_entry(path, Some("registered-mods"), k) + remove_entry(ini_path, Some("registered-mods"), k) .expect("Key is valid"); None } @@ -440,14 +523,15 @@ impl RegMod { }, Err(err) => { error!("Error: {err}"); - remove_entry(path, Some("registered-mods"), k).expect("Key is valid"); + remove_entry(ini_path, Some("registered-mods"), k).expect("Key is valid"); None } }) .collect()) } } - pub fn verify_state(&self, game_dir: &Path, ini_file: &Path) -> std::io::Result<()> { + + pub fn verify_state(&self, game_dir: &Path, ini_path: &Path) -> std::io::Result<()> { if (!self.state && self.mod_files.iter().any(FileData::is_enabled)) || (self.state && self.mod_files.iter().any(FileData::is_disabled)) { @@ -455,7 +539,7 @@ impl RegMod { "wrong file state for \"{}\" chaning file extentions", self.name ); - toggle_files(game_dir, self.state, self, Some(ini_file)).map(|_| ())? + toggle_files(game_dir, self.state, self, Some(ini_path)).map(|_| ())? } Ok(()) } diff --git a/tests/test_lib.rs b/tests/test_lib.rs index b52fe9f..db497bc 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -35,7 +35,7 @@ mod tests { ]; let test_mod = RegMod::new("Test", true, test_files.clone()); - let test_files_disabled = test_mod + let mut test_files_disabled = test_mod .mod_files .iter() .map(|file| PathBuf::from(format!("{}{OFF_STATE}", file.display()))) @@ -61,13 +61,9 @@ mod tests { assert!(file_exists(path_to_test.as_path())); } - let test_mod = RegMod { - name: test_mod.name, - state: false, - mod_files: test_files_disabled, - config_files: test_mod.config_files, - other_files: test_mod.other_files, - }; + test_files_disabled.extend(test_mod.config_files); + test_files_disabled.extend(test_mod.other_files); + let test_mod = RegMod::new(&test_mod.name, false, test_files_disabled); toggle_files( dir_to_test_files, diff --git a/ui/common.slint b/ui/common.slint index 70e70c5..02eb91f 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -23,9 +23,8 @@ export global MainLogic { callback remove-mod(string); callback edit-config([string]); callback edit-config-item(StandardListViewItem); - // consider using a string instead of an int for value - callback add-remove-order(bool, string, int) -> int; - callback modify-order(string, string, int) -> int; + callback add-remove-order(bool, string, string) -> int; + callback modify-order(string, string, string) -> int; callback force-app-focus(); callback force-deserialize(); callback send-message(Message); diff --git a/ui/tabs.slint b/ui/tabs.slint index e5f73c8..e1d6670 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -62,7 +62,7 @@ export component ModEdit inherits Tab { load-order-box := GroupBox { property temp; title: @tr("Load Order"); - enabled: MainLogic.current-mods[mod-index].dll-files.length > 0; + enabled: MainLogic.current-mods[mod-index].dll-files.length > 0 && SettingsLogic.loader-installed; function init-selected-index() { if MainLogic.current-mods[mod-index].dll-files.length != 1 { @@ -92,6 +92,7 @@ export component ModEdit inherits Tab { MainLogic.orders-set = MainLogic.orders-set + temp; MainLogic.current-mods[mod-index].order.set = self.checked; } else { + self.checked = !self.checked; MainLogic.force-deserialize() } temp = 0 @@ -103,7 +104,8 @@ export component ModEdit inherits Tab { MainLogic.orders-set = MainLogic.orders-set + temp; init-selected-index() } else { - MainLogic.force-deserialize(); + self.checked = !self.checked; + MainLogic.force-deserialize() } } if temp != 42069 { @@ -129,6 +131,9 @@ export component ModEdit inherits Tab { if temp == 1 { MainLogic.current-mods[mod-index].order.set = true; } + } else { + init-selected-index(); + MainLogic.force-deserialize() } temp = 0 } @@ -139,6 +144,8 @@ export component ModEdit inherits Tab { temp = MainLogic.modify-order(selected-dll, selected-dll, v - 1); if temp != -1 { MainLogic.orders-set = MainLogic.orders-set + temp + } else { + MainLogic.force-deserialize() } } if temp != -1 { From 05e7acb8c16192722bb1c289d470e2a1d8ab6747 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 30 Apr 2024 01:33:43 -0500 Subject: [PATCH 31/62] bug fix on logic of startup operation was deserializing data when game_dir was not valid, no need to do this it would always fail, now corrected it and fixed the logic for skipping the validation layer if mod_loader not installed --- src/main.rs | 21 ++++++++------------- ui/common.slint | 11 +++++++---- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0b8d2f6..2824a12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -131,12 +131,6 @@ fn main() -> Result<(), slint::PlatformError> { .into(), ); let _ = get_or_update_game_dir(Some(game_dir.clone().unwrap_or_default())); - deserialize_current_mods( - &RegMod::collect(current_ini, !game_verified).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), ui.as_weak() - ); let mod_loader: ModLoader; if !game_verified { ui.global::().set_current_subpage(1); @@ -149,6 +143,12 @@ fn main() -> Result<(), slint::PlatformError> { } else { let game_dir = game_dir.expect("game dir verified"); mod_loader = ModLoader::properties(&game_dir).unwrap_or_default(); + deserialize_current_mods( + &RegMod::collect(current_ini, !mod_loader.installed).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }), ui.as_weak() + ); ui.global::() .set_loader_disabled(mod_loader.disabled); if mod_loader.installed { @@ -171,12 +171,6 @@ fn main() -> Result<(), slint::PlatformError> { ui.global::().set_load_delay(SharedString::from(format!("{}ms", delay))); ui.global::().set_show_terminal(show_terminal); - - // Some(delay_time) => - // None => { - // error!("Found an unexpected character saved in \"load_delay\" Reseting to default value"); - // - // } } if !first_startup && !mod_loader.installed { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", &game_dir.display())); @@ -797,8 +791,9 @@ fn main() -> Result<(), slint::PlatformError> { match confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); + let loader_installed = ModLoader::properties(&game_dir).map_or(false, |d| d.installed); deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|err| { + &RegMod::collect(current_ini, !loader_installed).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }),ui.as_weak() diff --git a/ui/common.slint b/ui/common.slint index 02eb91f..2f98717 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -32,9 +32,11 @@ export global MainLogic { in-out property game-path-valid; in-out property orders-set; in-out property current-subpage: 0; - in-out property <[DisplayMod]> current-mods: [ - {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, order: {set: false}}, - ]; + in-out property <[DisplayMod]> current-mods; + // Placeholder data for easy live editing + // : [ + // {displayname: "Placeholder Name", name: "Placeholder Name", enabled: true, order: {set: false}}, + // ]; } export global SettingsLogic { @@ -45,7 +47,8 @@ export global SettingsLogic { callback toggle-terminal(bool) -> bool; callback set-load-delay(string); callback toggle-all(bool) -> bool; - in property game-path: "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; + in property game-path; + // : "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; in property loader-installed; in-out property dark-mode: true; in-out property loader-disabled; From ff1895d3f639fee19aeb362b4582c1c130d9f0dd Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 30 Apr 2024 11:34:57 -0500 Subject: [PATCH 32/62] cleaned up mod_loader api added whitespace in parser.rs for better readability --- src/main.rs | 51 ++++++++-------- src/utils/ini/mod_loader.rs | 114 ++++++++++++++++++++++++------------ src/utils/ini/parser.rs | 33 +++++++++-- ui/tabs.slint | 3 - 4 files changed, 130 insertions(+), 71 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2824a12..8ccfdf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -142,29 +142,29 @@ fn main() -> Result<(), slint::PlatformError> { mod_loader = ModLoader::default(); } else { let game_dir = game_dir.expect("game dir verified"); - mod_loader = ModLoader::properties(&game_dir).unwrap_or_default(); + mod_loader = ModLoader::properties(&game_dir); deserialize_current_mods( - &RegMod::collect(current_ini, !mod_loader.installed).unwrap_or_else(|err| { + &RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }), ui.as_weak() ); ui.global::() - .set_loader_disabled(mod_loader.disabled); - if mod_loader.installed { + .set_loader_disabled(mod_loader.disabled()); + if mod_loader.installed() { ui.global::().set_loader_installed(true); - let loader_cfg = ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[0]).unwrap(); + let loader_cfg = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[0]).unwrap(); let delay = loader_cfg.get_load_delay().unwrap_or_else(|err| { let err = format!("{err} Reseting to default value"); error!("{err}"); - save_value_ext(&mod_loader.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_VALUES[0]) + save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_VALUES[0]) .unwrap_or_else(|err| error!("{err}")); DEFAULT_VALUES[0].parse().unwrap() }); let show_terminal = loader_cfg.get_show_terminal().unwrap_or_else(|err| { let err = format!("{err} Reseting to default value"); error!("{err}"); - save_value_ext(&mod_loader.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_VALUES[1]) + save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_VALUES[1]) .unwrap_or_else(|err| error!("{err}")); false }); @@ -172,16 +172,20 @@ fn main() -> Result<(), slint::PlatformError> { ui.global::().set_load_delay(SharedString::from(format!("{}ms", delay))); ui.global::().set_show_terminal(show_terminal); } - if !first_startup && !mod_loader.installed { + // MARK: BUG? + // sometimes messages in the startup process are not centered + // most likely because we are trying to calculate the position data before slint event loop as been initialized with ui.run() + // try invoke from event loop? + if !first_startup && !mod_loader.installed() { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", &game_dir.display())); } } if first_startup { - if !game_verified && !mod_loader.installed { + if !game_verified && !mod_loader.installed() { ui.display_msg( "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease select the game directory containing \"eldenring.exe\"", ); - } else if game_verified && !mod_loader.installed { + } else if game_verified && !mod_loader.installed() { ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app"); } else if game_verified { let ui_handle = ui.as_weak(); @@ -380,22 +384,15 @@ fn main() -> Result<(), slint::PlatformError> { return; }; info!("Success: Files found, saved diretory"); - let mod_loader = match ModLoader::properties(&try_path) { - Ok(loader) => loader, - Err(err) => { - error!("{err}"); - ui.display_msg(&err.to_string()); - return; - } - }; + let mod_loader = ModLoader::properties(&try_path); ui.global::() .set_game_path(try_path.to_string_lossy().to_string().into()); let _ = get_or_update_game_dir(Some(try_path)); ui.global::().set_game_path_valid(true); ui.global::().set_current_subpage(0); - ui.global::().set_loader_installed(mod_loader.installed); - ui.global::().set_loader_disabled(mod_loader.disabled); - if mod_loader.installed { + ui.global::().set_loader_installed(mod_loader.installed()); + ui.global::().set_loader_disabled(mod_loader.disabled()); + if mod_loader.installed() { ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!") } else { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") @@ -791,9 +788,9 @@ fn main() -> Result<(), slint::PlatformError> { match confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); - let loader_installed = ModLoader::properties(&game_dir).map_or(false, |d| d.installed); + let mod_loader = ModLoader::properties(&game_dir); deserialize_current_mods( - &RegMod::collect(current_ini, !loader_installed).unwrap_or_else(|err| { + &RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }),ui.as_weak() @@ -814,7 +811,7 @@ fn main() -> Result<(), slint::PlatformError> { let error = 42069_i32; let game_dir = get_or_update_game_dir(None); let mut result: i32 = if state { 1 } else { -1 }; - let mut load_order = match ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) { + let mut load_order = match ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err); @@ -853,7 +850,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); let mut result = 0_i32; let game_dir = get_or_update_game_dir(None); - let mut load_order = match ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) { + let mut load_order = match ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err); @@ -1055,15 +1052,15 @@ fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { files: ModelRc::from(files), config_files: ModelRc::from(config_files), dll_files: ModelRc::from(dll_files), - // MARK: TODO - // need to be able to sort RegMods by load-order then albethabetical order: LoadOrder { at: mod_data.order.at as i32 + 1, i: mod_data.order.i as i32, set: mod_data.order.set }, }) } ui.global::().set_current_mods(ModelRc::from(display_mods)); ui.global::().set_orders_set(mods.order_count() as i32); } + // MARK: TODO +// need to be able to sort RegMods by load-order then albethabetical // need to use ModelNotify::row_changed to handle updating page info on change // ui.invoke_update_mod_index(1, 1); diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index d823432..e3be3d9 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -1,57 +1,75 @@ use ini::Ini; -use log::{error, info, warn}; +use log::{error, info, trace}; use std::path::{Path, PathBuf}; use crate::{ - utils::ini::parser::{IniProperty, RegMod}, - utils::ini::writer::EXT_OPTIONS, + utils::ini::{ + parser::{IniProperty, RegMod}, + writer::EXT_OPTIONS, + }, LOADER_KEYS, LOADER_SECTIONS, {does_dir_contain, get_cfg, Operation, LOADER_FILES, LOADER_FILES_DISABLED}, }; #[derive(Default)] pub struct ModLoader { - pub installed: bool, - pub disabled: bool, - pub cfg: PathBuf, + installed: bool, + disabled: bool, + path: PathBuf, } impl ModLoader { - pub fn properties(game_dir: &Path) -> std::io::Result { - let disabled: bool; - let cfg: PathBuf; - let installed = match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { + pub fn properties(game_dir: &Path) -> ModLoader { + match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { Ok(true) => { info!("Found mod loader files"); - cfg = game_dir.join(LOADER_FILES[0]); - disabled = false; - true + ModLoader { + installed: true, + disabled: false, + path: game_dir.join(LOADER_FILES[0]), + } } Ok(false) => { - warn!("Checking if mod loader is disabled"); + trace!("Checking if mod loader is disabled"); match does_dir_contain(game_dir, Operation::All, &LOADER_FILES_DISABLED) { Ok(true) => { info!("Found mod loader files in the disabled state"); - cfg = game_dir.join(LOADER_FILES[0]); - disabled = true; - true + ModLoader { + installed: true, + disabled: true, + path: game_dir.join(LOADER_FILES[0]), + } } Ok(false) => { error!("Mod Loader Files not found in selected path"); - cfg = PathBuf::new(); - disabled = false; - false + ModLoader::default() + } + Err(err) => { + error!("{err}"); + ModLoader::default() } - Err(err) => return Err(err), } } - Err(err) => return Err(err), - }; - Ok(ModLoader { - installed, - disabled, - cfg, - }) + Err(err) => { + error!("{err}"); + ModLoader::default() + } + } + } + + #[inline] + pub fn installed(&self) -> bool { + self.installed + } + + #[inline] + pub fn disabled(&self) -> bool { + self.disabled + } + + #[inline] + pub fn path(&self) -> &Path { + &self.path } } @@ -63,7 +81,7 @@ pub struct ModLoaderCfg { } impl ModLoaderCfg { - pub fn load(game_dir: &Path, section: Option<&str>) -> Result { + pub fn read_section(game_dir: &Path, section: Option<&str>) -> Result { if section.is_none() { return Err(String::from("section can not be none")); } @@ -81,13 +99,7 @@ impl ModLoaderCfg { Err(err) => return Err(format!("Could not read \"mod_loader_config.ini\"\n{err}")), }; if cfg.section(section).is_none() { - cfg.with_section(section).set("setter_temp_val", "0"); - if cfg.delete_from(section, "setter_temp_val").is_none() { - return Err(format!( - "Failed to create a new section: \"{}\"", - section.unwrap() - )); - }; + ModLoaderCfg::init_section(&mut cfg, section)? } Ok(ModLoaderCfg { cfg, @@ -96,6 +108,28 @@ impl ModLoaderCfg { }) } + pub fn update_section(&mut self, section: Option<&str>) -> Result<(), String> { + if self.cfg.section(section).is_none() { + ModLoaderCfg::init_section(&mut self.cfg, section)? + }; + Ok(()) + } + + fn init_section(cfg: &mut ini::Ini, section: Option<&str>) -> Result<(), String> { + trace!( + "Section: \"{}\" not found creating new", + section.expect("Passed in section not valid") + ); + cfg.with_section(section).set("setter_temp_val", "0"); + if cfg.delete_from(section, "setter_temp_val").is_none() { + return Err(format!( + "Failed to create a new section: \"{}\"", + section.unwrap() + )); + }; + Ok(()) + } + pub fn get_load_delay(&self) -> Result { match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], false) { Some(delay_time) => Ok(delay_time.value), @@ -116,19 +150,23 @@ impl ModLoaderCfg { } } + #[inline] pub fn mut_section(&mut self) -> &mut ini::Properties { self.cfg.section_mut(self.section.as_ref()).unwrap() } + #[inline] fn section(&self) -> &ini::Properties { self.cfg.section(self.section.as_ref()).unwrap() } + #[inline] fn iter(&self) -> ini::PropertyIter { self.section().iter() } - pub fn parse(&self) -> Result, std::num::ParseIntError> { + /// Returns an owned `Vec` with values parsed into `usize` + pub fn parse_section(&self) -> Result, std::num::ParseIntError> { self.iter() .map(|(k, v)| { let parse_v = v.parse::(); @@ -137,10 +175,12 @@ impl ModLoaderCfg { .collect::, _>>() } + #[inline] pub fn is_empty(&self) -> bool { self.section().is_empty() } + #[inline] pub fn dir(&self) -> &Path { &self.cfg_dir } diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index fe1d332..fdc6fa0 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -18,12 +18,14 @@ use crate::{ pub trait ValueType: Sized { type ParseError: std::fmt::Display; + fn parse_str( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, ) -> Result; + #[allow(unused_variables)] fn validate( self, @@ -37,6 +39,7 @@ pub trait ValueType: Sized { impl ValueType for bool { type ParseError = ParseBoolError; + fn parse_str( ini: &Ini, section: Option<&str>, @@ -56,6 +59,7 @@ impl ValueType for bool { impl ValueType for u32 { type ParseError = std::num::ParseIntError; + fn parse_str( ini: &Ini, section: Option<&str>, @@ -70,6 +74,7 @@ impl ValueType for u32 { impl ValueType for PathBuf { type ParseError = std::io::Error; + fn parse_str( ini: &Ini, section: Option<&str>, @@ -86,6 +91,7 @@ impl ValueType for PathBuf { parsed_value.validate(ini, section, skip_validation) } } + fn validate(self, ini: &Ini, section: Option<&str>, disable: bool) -> std::io::Result { if !disable { if section == Some("mod-files") { @@ -108,6 +114,7 @@ impl ValueType for PathBuf { impl ValueType for Vec { type ParseError = std::io::Error; + fn parse_str( ini: &Ini, section: Option<&str>, @@ -123,6 +130,7 @@ impl ValueType for Vec { .map(|(_, v)| PathBuf::from(v)) .collect() } + let parsed_value = read_array( ini.section(section) .expect("Validated by IniProperty::is_valid"), @@ -134,6 +142,7 @@ impl ValueType for Vec { parsed_value.validate(ini, section, skip_validation) } } + fn validate(self, ini: &Ini, _section: Option<&str>, disable: bool) -> std::io::Result { if !disable { let game_dir = match IniProperty::::read(ini, Some("paths"), "game_dir", false) @@ -207,7 +216,7 @@ impl IniProperty { Ok(value) => { trace!( "Success: read key: \"{key}\" Section: \"{}\" from ini", - section.expect("Passed in section should be valid") + section.expect("Passed in section not valid") ); Some(IniProperty { //section: Some(section.unwrap().to_string()), @@ -220,7 +229,7 @@ impl IniProperty { "{}", format!( "Value stored in Section: \"{}\", Key: \"{key}\" is not valid", - section.expect("Passed in section should be valid") + section.expect("Passed in section not valid") ) ); error!("Error: {err}"); @@ -317,6 +326,7 @@ impl RegMod { }); (mod_files, config_files, other_files) } + /// This function omits the population of the `order` field pub fn new(name: &str, state: bool, in_files: Vec) -> Self { let (mod_files, config_files, other_files) = RegMod::split_out_config_files(in_files); @@ -329,6 +339,7 @@ impl RegMod { order: LoadOrder::default(), } } + /// This function populates all fields fn new_full( name: &str, @@ -363,8 +374,10 @@ impl RegMod { order, }) } + pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { type ModData<'a> = Vec<(&'a str, Result, Vec)>; + fn sync_keys<'a>(ini: &'a Ini, ini_path: &Path) -> std::io::Result> { fn collect_file_data(section: &Properties) -> HashMap<&str, Vec<&str>> { section @@ -382,6 +395,7 @@ impl RegMod { }) .collect() } + fn combine_map_data<'a>( state_map: HashMap<&'a str, &str>, file_map: HashMap<&str, Vec<&str>>, @@ -401,6 +415,7 @@ impl RegMod { mod_data.sort_by_key(|(key, _, _)| *key); mod_data } + let mod_state_data = ini .section(Some("registered-mods")) .expect("Validated by Ini::is_setup on startup"); @@ -414,16 +429,19 @@ impl RegMod { .filter(|k| !file_data.contains_key(*k)) .cloned() .collect::>(); + for key in invalid_state { state_data.remove(key); remove_entry(ini_path, Some("registered-mods"), key)?; warn!("\"{key}\" has no matching files"); } + let invalid_files = file_data .keys() .filter(|k| !state_data.contains_key(*k)) .cloned() .collect::>(); + for key in invalid_files { if file_data.get(key).expect("key exists").len() > 1 { remove_array(ini_path, key)?; @@ -433,8 +451,10 @@ impl RegMod { file_data.remove(key); warn!("\"{key}\" has no matching state"); } + Ok(combine_map_data(state_data, file_data)) } + fn collect_data_unsafe(ini: &Ini) -> Vec<(&str, &str, Vec<&str>)> { let mod_state_data = ini .section(Some("registered-mods")) @@ -458,6 +478,7 @@ impl RegMod { }) .collect() } + let ini = get_cfg(ini_path)?; if skip_validation { @@ -480,9 +501,9 @@ impl RegMod { "Could not read \"game_dir\" from file", ))? .value; - let mut load_order_parsed = ModLoaderCfg::load(&game_dir, LOADER_SECTIONS[1]) + let mut load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))? - .parse() + .parse_section() .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?; Ok(parsed_data .iter() @@ -543,6 +564,7 @@ impl RegMod { } Ok(()) } + pub fn file_refs(&self) -> Vec<&Path> { let mut path_refs = Vec::with_capacity(self.all_files_len()); path_refs.extend(self.mod_files.iter().map(|f| f.as_path())); @@ -550,6 +572,7 @@ impl RegMod { path_refs.extend(self.other_files.iter().map(|f| f.as_path())); path_refs } + pub fn add_other_files_to_files<'a>(&'a self, files: &'a [PathBuf]) -> Vec<&'a Path> { let mut path_refs = Vec::with_capacity(files.len() + self.other_files_len()); path_refs.extend(files.iter().map(|f| f.as_path())); @@ -557,9 +580,11 @@ impl RegMod { path_refs.extend(self.other_files.iter().map(|f| f.as_path())); path_refs } + pub fn all_files_len(&self) -> usize { self.mod_files.len() + self.config_files.len() + self.other_files.len() } + pub fn other_files_len(&self) -> usize { self.config_files.len() + self.other_files.len() } diff --git a/ui/tabs.slint b/ui/tabs.slint index e1d6670..e43e898 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -56,9 +56,6 @@ export component ModEdit inherits Tab { padding-bottom: Formatting.side-padding / 2; alignment: space-between; - // MARK: TODO - // inbed load_order data inside RegMod::collect for init deserialize - // need to use ModelNotify::row_changed to handle updating page info on change load-order-box := GroupBox { property temp; title: @tr("Load Order"); From b1e24cc93be3bd0079ad35e1da0ff78140ccd576 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 30 Apr 2024 15:01:42 -0500 Subject: [PATCH 33/62] Bug fix, startup messages are now correct we now collect all error messages in startup logic into a Vec then spawnup a thread and wait for the slint event loop to be initialized. then we wait on user to click through before displaying any welcome messages this fixes two bugs, a. where the position data was not being calculated correctly causing the popup message not to be centered b. welcome messages would overwrite the state of any previous error error messages because we were not wating for user input between messages --- src/main.rs | 165 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 61 deletions(-) diff --git a/src/main.rs b/src/main.rs index 8ccfdf7..ab1fa61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,12 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -// #![windows_subsystem = "windows"] +#![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ ini::{ mod_loader::{ModLoader, ModLoaderCfg, update_order_entries, Countable}, - parser::{file_registered, IniProperty, RegMod, Valitidity}, + parser::{file_registered, IniProperty, RegMod, Valitidity, ErrorClone}, writer::*, }, installer::{remove_mod_files, InstallData, scan_for_mods} @@ -55,6 +55,7 @@ fn main() -> Result<(), slint::PlatformError> { { let current_ini = get_ini_dir(); let first_startup: bool; + let mut errors= Vec::new(); let ini_valid = match get_cfg(current_ini) { Ok(ini) => { if ini.is_setup() { @@ -67,7 +68,9 @@ fn main() -> Result<(), slint::PlatformError> { } } Err(err) => { + // io::Open error or | parse error with type ErrorKind::InvalidData error!("Error: {err}"); + errors.push(err); first_startup = true; false } @@ -78,30 +81,37 @@ fn main() -> Result<(), slint::PlatformError> { } let game_verified: bool; + let mut reg_mods = None; let game_dir = match attempt_locate_game(current_ini) { Ok(path_result) => match path_result { - PathResult::Full(path) => match RegMod::collect(current_ini, false) { - Ok(reg_mods) => { + PathResult::Full(path) => { + reg_mods = Some(RegMod::collect(current_ini, false)); + match reg_mods { + Some(Ok(ref reg_mods)) => { reg_mods.iter().for_each(|data| { data.verify_state(&path, current_ini) - .unwrap_or_else(|err| ui.display_msg(&err.to_string())) + // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error + .unwrap_or_else(|err| errors.push(err)) }); game_verified = true; Some(path) } - Err(err) => { - ui.display_msg(&err.to_string()); + Some(Err(ref err)) => { + // io::Write error + errors.push(err.clone_err()); game_verified = true; Some(path) } - }, + None => unreachable!() + }}, PathResult::Partial(path) | PathResult::None(path) => { game_verified = false; Some(path) } }, Err(err) => { - ui.display_msg(&err.to_string()); + // io::Write error + errors.push(err); game_verified = false; None } @@ -117,7 +127,8 @@ fn main() -> Result<(), slint::PlatformError> { None => { ui.global::().set_dark_mode(true); save_bool(current_ini, Some("app-settings"), "dark_mode", true) - .unwrap_or_else(|err| ui.display_msg(&err.to_string())); + // io::Write error + .unwrap_or_else(|err| errors.push(err)); } }; @@ -131,86 +142,118 @@ fn main() -> Result<(), slint::PlatformError> { .into(), ); let _ = get_or_update_game_dir(Some(game_dir.clone().unwrap_or_default())); + let mod_loader: ModLoader; if !game_verified { ui.global::().set_current_subpage(1); - if !first_startup { - ui.display_msg( - "Failed to locate Elden Ring\nPlease Select the install directory for Elden Ring", - ); - } mod_loader = ModLoader::default(); } else { let game_dir = game_dir.expect("game dir verified"); mod_loader = ModLoader::properties(&game_dir); deserialize_current_mods( - &RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), ui.as_weak() + &match reg_mods { + Some(Ok(mod_data)) => mod_data, + _ => RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { + // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error + errors.push(err); + vec![RegMod::default()] + }) + },ui.as_weak() ); - ui.global::() - .set_loader_disabled(mod_loader.disabled()); + ui.global::().set_loader_disabled(mod_loader.disabled()); + if mod_loader.installed() { ui.global::().set_loader_installed(true); let loader_cfg = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[0]).unwrap(); - let delay = loader_cfg.get_load_delay().unwrap_or_else(|err| { - let err = format!("{err} Reseting to default value"); + let delay = loader_cfg.get_load_delay().unwrap_or_else(|_| { + // parse error ErrorKind::InvalidData + let err = std::io::Error::new(ErrorKind::InvalidData, format!( + "Found an unexpected character saved in \"{}\" Reseting to default value", + LOADER_KEYS[0] + )); error!("{err}"); + errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_VALUES[0]) - .unwrap_or_else(|err| error!("{err}")); + .unwrap_or_else(|err| { + // io::write error + error!("{err}"); + errors.push(err); + }); DEFAULT_VALUES[0].parse().unwrap() }); - let show_terminal = loader_cfg.get_show_terminal().unwrap_or_else(|err| { - let err = format!("{err} Reseting to default value"); + let show_terminal = loader_cfg.get_show_terminal().unwrap_or_else(|_| { + // parse error ErrorKind::InvalidData + let err = std::io::Error::new(ErrorKind::InvalidData, format!( + "Found an unexpected character saved in \"{}\" Reseting to default value", + LOADER_KEYS[1] + )); error!("{err}"); + errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_VALUES[1]) - .unwrap_or_else(|err| error!("{err}")); + .unwrap_or_else(|err| { + // io::write error + error!("{err}"); + errors.push(err); + }); false }); ui.global::().set_load_delay(SharedString::from(format!("{}ms", delay))); ui.global::().set_show_terminal(show_terminal); } - // MARK: BUG? - // sometimes messages in the startup process are not centered - // most likely because we are trying to calculate the position data before slint event loop as been initialized with ui.run() - // try invoke from event loop? - if !first_startup && !mod_loader.installed() { - ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", &game_dir.display())); - } } - if first_startup { - if !game_verified && !mod_loader.installed() { - ui.display_msg( - "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease select the game directory containing \"eldenring.exe\"", - ); - } else if game_verified && !mod_loader.installed() { - ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app"); - } else if game_verified { - let ui_handle = ui.as_weak(); + // we need to wait for slint event loop to start `ui.run()` before making calls to `ui.display_msg()` + // otherwise calculations for the positon of display_msg_popup are not correct + let ui_handle = ui.as_weak(); + let _ = std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(200)); + slint::invoke_from_event_loop(move || { slint::spawn_local(async move { let ui = ui_handle.unwrap(); - match confirm_scan_mods(ui.as_weak(), &get_or_update_game_dir(None), current_ini, false).await { - Ok(len) => { - deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), ui.as_weak() - ); - ui.display_msg(&format!("Successfully Found {len} mod(s)")); + if !errors.is_empty() { + for err in errors { + ui.display_msg(&err.to_string()); let _ = receive_msg().await; } - Err(err) => if err.kind() != ErrorKind::ConnectionAborted { - ui.display_msg(&format!("Error: {err}")); - let _ = receive_msg().await; + } + if first_startup { + if !game_verified && !mod_loader.installed() { + ui.display_msg( + "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease select the game directory containing \"eldenring.exe\"", + ); + } else if game_verified && !mod_loader.installed() { + ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app"); + } else if game_verified { + match confirm_scan_mods(ui.as_weak(), &get_or_update_game_dir(None), current_ini, false).await { + Ok(len) => { + deserialize_current_mods( + &RegMod::collect(current_ini, false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }), ui.as_weak() + ); + ui.display_msg(&format!("Successfully Found {len} mod(s)")); + let _ = receive_msg().await; + } + Err(err) => if err.kind() != ErrorKind::ConnectionAborted { + ui.display_msg(&format!("Error: {err}")); + let _ = receive_msg().await; + } + }; + ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); } - }; - ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); + } else if game_verified { + if !mod_loader.installed() { + ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", get_or_update_game_dir(None).display())); + } + } else { + ui.display_msg( + "Failed to locate Elden Ring\nPlease Select the install directory for Elden Ring", + ); + } }).unwrap(); - } - } + }).unwrap(); + }); } // TODO: Error check input text for invalid symbols @@ -814,7 +857,7 @@ fn main() -> Result<(), slint::PlatformError> { let mut load_order = match ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { - ui.display_msg(&err); + ui.display_msg(&err.to_string()); return error; } }; @@ -853,7 +896,7 @@ fn main() -> Result<(), slint::PlatformError> { let mut load_order = match ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { - ui.display_msg(&err); + ui.display_msg(&err.to_string()); return -1; } }; From b548595cb4850429ab7e0cad7041488a9d540b53 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 30 Apr 2024 15:06:01 -0500 Subject: [PATCH 34/62] api cleanup Moved mod_loader to return io::errors to match the rest of the apis added some convienince functions for errors --- src/lib.rs | 5 ++- src/utils/ini/mod_loader.rs | 66 +++++++++++++++++++++++-------------- src/utils/ini/parser.rs | 25 +++++++++++++- 3 files changed, 67 insertions(+), 29 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e6dedc0..080ed99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ pub mod utils { use ini::Ini; use log::{error, info, trace, warn}; use utils::ini::{ - parser::{IniProperty, RegMod}, + parser::{IniProperty, IntoIoError, RegMod}, writer::{remove_array, save_bool, save_path, save_path_bufs}, }; @@ -179,8 +179,7 @@ pub fn toggle_files( } pub fn get_cfg(input_file: &Path) -> std::io::Result { - Ini::load_from_file_noescape(input_file) - .map_err(|err| std::io::Error::new(ErrorKind::AddrNotAvailable, err)) + Ini::load_from_file_noescape(input_file).map_err(|err| err.into_io_error()) } pub enum Operation { diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index e3be3d9..de42778 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -1,8 +1,12 @@ use ini::Ini; use log::{error, info, trace}; -use std::path::{Path, PathBuf}; +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, +}; use crate::{ + new_io_error, utils::ini::{ parser::{IniProperty, RegMod}, writer::EXT_OPTIONS, @@ -81,22 +85,28 @@ pub struct ModLoaderCfg { } impl ModLoaderCfg { - pub fn read_section(game_dir: &Path, section: Option<&str>) -> Result { + pub fn read_section(game_dir: &Path, section: Option<&str>) -> std::io::Result { if section.is_none() { - return Err(String::from("section can not be none")); + return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); } let cfg_dir = match does_dir_contain(game_dir, Operation::All, &[LOADER_FILES[0]]) { Ok(true) => game_dir.join(LOADER_FILES[0]), Ok(false) => { - return Err(String::from( - "\"mod_loader_config.ini\" does not exist in the current game_dir", - )) + return new_io_error!( + ErrorKind::NotFound, + "\"mod_loader_config.ini\" does not exist in the current game_dir" + ); } - Err(err) => return Err(err.to_string()), + Err(err) => return Err(err), }; let mut cfg = match get_cfg(&cfg_dir) { Ok(ini) => ini, - Err(err) => return Err(format!("Could not read \"mod_loader_config.ini\"\n{err}")), + Err(err) => { + return new_io_error!( + ErrorKind::NotFound, + format!("Could not read \"mod_loader_config.ini\"\n{err}") + ) + } }; if cfg.section(section).is_none() { ModLoaderCfg::init_section(&mut cfg, section)? @@ -108,45 +118,51 @@ impl ModLoaderCfg { }) } - pub fn update_section(&mut self, section: Option<&str>) -> Result<(), String> { + pub fn update_section(&mut self, section: Option<&str>) -> std::io::Result<()> { if self.cfg.section(section).is_none() { ModLoaderCfg::init_section(&mut self.cfg, section)? }; Ok(()) } - fn init_section(cfg: &mut ini::Ini, section: Option<&str>) -> Result<(), String> { + fn init_section(cfg: &mut ini::Ini, section: Option<&str>) -> std::io::Result<()> { trace!( "Section: \"{}\" not found creating new", section.expect("Passed in section not valid") ); cfg.with_section(section).set("setter_temp_val", "0"); if cfg.delete_from(section, "setter_temp_val").is_none() { - return Err(format!( - "Failed to create a new section: \"{}\"", - section.unwrap() - )); + return new_io_error!( + ErrorKind::BrokenPipe, + format!("Failed to create a new section: \"{}\"", section.unwrap()) + ); }; Ok(()) } - pub fn get_load_delay(&self) -> Result { + pub fn get_load_delay(&self) -> std::io::Result { match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], false) { Some(delay_time) => Ok(delay_time.value), - None => Err(format!( - "Found an unexpected character saved in \"{}\"", - LOADER_KEYS[0] - )), + None => new_io_error!( + ErrorKind::InvalidData, + format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[0] + ) + ), } } - pub fn get_show_terminal(&self) -> Result { + pub fn get_show_terminal(&self) -> std::io::Result { match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1], false) { Some(delay_time) => Ok(delay_time.value), - None => Err(format!( - "Found an unexpected character saved in \"{}\"", - LOADER_KEYS[0] - )), + None => new_io_error!( + ErrorKind::InvalidData, + format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[1] + ) + ), } } @@ -181,7 +197,7 @@ impl ModLoaderCfg { } #[inline] - pub fn dir(&self) -> &Path { + pub fn path(&self) -> &Path { &self.cfg_dir } diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index fdc6fa0..538f62f 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -219,7 +219,7 @@ impl IniProperty { section.expect("Passed in section not valid") ); Some(IniProperty { - //section: Some(section.unwrap().to_string()), + //section: section.map(String::from), //key: key.to_string(), value, }) @@ -600,6 +600,29 @@ pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { }) } +pub trait IntoIoError { + fn into_io_error(self) -> std::io::Error; +} + +impl IntoIoError for ini::Error { + fn into_io_error(self) -> std::io::Error { + match self { + ini::Error::Io(err) => err, + ini::Error::Parse(err) => std::io::Error::new(ErrorKind::InvalidData, err), + } + } +} + +pub trait ErrorClone { + fn clone_err(&self) -> std::io::Error; +} + +impl ErrorClone for std::io::Error { + fn clone_err(&self) -> std::io::Error { + std::io::Error::new(self.kind(), self.to_string()) + } +} + // ----------------------Optimized original implementation------------------------------- // let mod_state_data = ini.section(Some("registered-mods")).unwrap(); // mod_state_data From a7b7af9ce833903b8be0c01934a0900cf75c4958 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 1 May 2024 02:22:02 -0500 Subject: [PATCH 35/62] updates to the parser api changed a lot in parser to be cleaner overall, now can propagate up io::Errors cleanly, convert and add detials to Error messages. now RegMod::collect() will sort by load order then alphabetical changed the structure of RegMod so it is easier for the internal api to work with making the code much clenaer and easier to work with updated tests and rest of code base to reflect these changes --- src/utils/ini/parser.rs | 567 +++++++++++++++++++++------------------- tests/test_ini_tools.rs | 6 +- tests/test_lib.rs | 13 +- 3 files changed, 301 insertions(+), 285 deletions(-) diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 538f62f..a36d89c 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -1,10 +1,9 @@ use ini::{Ini, Properties}; -use log::{error, trace, warn}; +use log::{error, warn}; use std::{ collections::HashMap, io::ErrorKind, path::{Path, PathBuf}, - str::ParseBoolError, }; use crate::{ @@ -16,65 +15,51 @@ use crate::{ FileData, LOADER_SECTIONS, OFF_STATE, }; -pub trait ValueType: Sized { - type ParseError: std::fmt::Display; - +pub trait Parsable: Sized { fn parse_str( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, - ) -> Result; - - #[allow(unused_variables)] - fn validate( - self, - ini: &Ini, - section: Option<&str>, - disable: bool, - ) -> Result { - Ok(self) - } + ) -> std::io::Result; } -impl ValueType for bool { - type ParseError = ParseBoolError; - +impl Parsable for bool { fn parse_str( ini: &Ini, section: Option<&str>, key: &str, _skip_validation: bool, - ) -> Result { + ) -> std::io::Result { match ini .get_from(section, key) .expect("Validated by IniProperty::is_valid") { "0" => Ok(false), "1" => Ok(true), - c => c.to_lowercase().parse::(), + c => c + .to_lowercase() + .parse::() + .map_err(|err| err.into_io_error()), } } } -impl ValueType for u32 { - type ParseError = std::num::ParseIntError; - +impl Parsable for u32 { fn parse_str( ini: &Ini, section: Option<&str>, key: &str, _skip_validation: bool, - ) -> Result { + ) -> std::io::Result { ini.get_from(section, key) .expect("Validated by IniProperty::is_valid") .parse::() + .map_err(|err| err.into_io_error()) } } -impl ValueType for PathBuf { - type ParseError = std::io::Error; - +impl Parsable for PathBuf { fn parse_str( ini: &Ini, section: Option<&str>, @@ -87,34 +72,15 @@ impl ValueType for PathBuf { ); if skip_validation { Ok(parsed_value) + } else if let Err(err) = parsed_value.as_path().validate(ini, section) { + Err(err) } else { - parsed_value.validate(ini, section, skip_validation) - } - } - - fn validate(self, ini: &Ini, section: Option<&str>, disable: bool) -> std::io::Result { - if !disable { - if section == Some("mod-files") { - let game_dir = - match IniProperty::::read(ini, Some("paths"), "game_dir", false) { - Some(ini_property) => ini_property.value, - None => return new_io_error!(ErrorKind::NotFound, "game_dir is not valid"), - }; - validate_file(&game_dir.join(&self))?; - Ok(self) - } else { - validate_existance(&self)?; - Ok(self) - } - } else { - Ok(self) + Ok(parsed_value) } } } -impl ValueType for Vec { - type ParseError = std::io::Error; - +impl Parsable for Vec { fn parse_str( ini: &Ini, section: Option<&str>, @@ -138,28 +104,56 @@ impl ValueType for Vec { ); if skip_validation { Ok(parsed_value) + } else if let Some(err) = parsed_value + .iter() + .find_map(|f| f.as_path().validate(ini, section).err()) + { + Err(err) } else { - parsed_value.validate(ini, section, skip_validation) + Ok(parsed_value) } } +} - fn validate(self, ini: &Ini, _section: Option<&str>, disable: bool) -> std::io::Result { - if !disable { - let game_dir = match IniProperty::::read(ini, Some("paths"), "game_dir", false) - { - Some(ini_property) => ini_property.value, - None => return new_io_error!(ErrorKind::NotFound, "game_dir is not valid"), - }; - if let Some(err) = self - .iter() - .find_map(|path| validate_file(&game_dir.join(path)).err()) - { - Err(err) - } else { - Ok(self) - } +pub trait Valitidity { + fn is_setup(&self) -> bool { + true + } + #[allow(unused_variables)] + fn validate(&self, ini: &Ini, section: Option<&str>) -> std::io::Result<()> { + Ok(()) + } +} + +impl Valitidity for Ini { + fn is_setup(&self) -> bool { + INI_SECTIONS.iter().all(|section| { + let trimmed_section: String = section.trim_matches(|c| c == '[' || c == ']').to_owned(); + self.section(Some(trimmed_section)).is_some() + }) + } +} + +impl Valitidity for &Path { + fn validate(&self, ini: &Ini, section: Option<&str>) -> std::io::Result<()> { + if section == Some("mod-files") { + let game_dir = + IniProperty::::read(ini, Some("paths"), "game_dir", false)?.value; + validate_file(&game_dir.join(self))?; + Ok(()) } else { - Ok(self) + validate_existance(self)?; + Ok(()) + } + } +} + +impl Valitidity for Vec<&Path> { + fn validate(&self, ini: &Ini, section: Option<&str>) -> std::io::Result<()> { + if let Some(err) = self.iter().find_map(|f| f.validate(ini, section).err()) { + Err(err) + } else { + Ok(()) } } } @@ -199,43 +193,24 @@ fn validate_existance(path: &Path) -> std::io::Result<()> { } } -pub struct IniProperty { +pub struct IniProperty { //section: Option, //key: String, pub value: T, } -impl IniProperty { +impl IniProperty { pub fn read( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, - ) -> Option> { - match IniProperty::is_valid(ini, section, key, skip_validation) { - Ok(value) => { - trace!( - "Success: read key: \"{key}\" Section: \"{}\" from ini", - section.expect("Passed in section not valid") - ); - Some(IniProperty { - //section: section.map(String::from), - //key: key.to_string(), - value, - }) - } - Err(err) => { - error!( - "{}", - format!( - "Value stored in Section: \"{}\", Key: \"{key}\" is not valid", - section.expect("Passed in section not valid") - ) - ); - error!("Error: {err}"); - None - } - } + ) -> std::io::Result> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, skip_validation)?, + }) } fn is_valid( @@ -243,143 +218,200 @@ impl IniProperty { section: Option<&str>, key: &str, skip_validation: bool, - ) -> Result { + ) -> std::io::Result { match &ini.section(section) { Some(s) => match s.contains_key(key) { - true => { - T::parse_str(ini, section, key, skip_validation).map_err(|err| err.to_string()) - } - false => Err(format!("Key: \"{key}\" not found in {ini:?}")), + true => T::parse_str(ini, section, key, skip_validation), + false => new_io_error!( + ErrorKind::NotFound, + format!("Key: \"{key}\" not found in {ini:?}") + ), }, - None => Err(format!( - "Section: \"{}\" not found in {ini:?}", - section.expect("Passed in section should be valid") - )), + None => new_io_error!( + ErrorKind::NotFound, + format!( + "Section: \"{}\" not found in {ini:?}", + section.expect("Passed in section should be valid") + ) + ), } } } -pub trait Valitidity { - fn is_setup(&self) -> bool; -} - -impl Valitidity for Ini { - fn is_setup(&self) -> bool { - INI_SECTIONS.iter().all(|section| { - let trimmed_section: String = section.trim_matches(|c| c == '[' || c == ']').to_owned(); - self.section(Some(trimmed_section)).is_some() - }) - } -} - #[derive(Default)] pub struct RegMod { - /// Key in snake_case + /// user defined Key in snake_case pub name: String, /// true = enabled | false = disabled pub state: bool, + /// files associated with the Registered Mod + pub files: SplitFiles, + + /// contains properties related to if a mod has a set load order + pub order: LoadOrder, +} + +#[derive(Default)] +pub struct SplitFiles { /// files with extension `.dll` | also possible they end in `.dll.disabled` /// saved as short paths with `game_dir` truncated - pub mod_files: Vec, + pub dll: Vec, /// files with extension `.ini` /// saved as short paths with `game_dir` truncated - pub config_files: Vec, + pub config: Vec, /// files with any extension other than `.dll` or `.ini` /// saved as short paths with `game_dir` truncated - pub other_files: Vec, - - /// contains properties related to if a mod has a set load order - pub order: LoadOrder, + pub other: Vec, } #[derive(Default)] pub struct LoadOrder { - /// if one of `self.mod_files` has a set load_order + /// if one of `SplitFiles.dll` has a set load_order pub set: bool, - /// the index of the selected `.dll` within `self.mod_files` + /// the index of the selected `mod_file` within `SplitFiles.dll` pub i: usize, /// current set value of `load_order` - /// `self.order.at` is stored as 0 index | front end uses 1 index + /// `self.at` is stored as 0 index | front end uses 1 index pub at: usize, } -impl RegMod { - fn split_out_config_files( - in_files: Vec, - ) -> (Vec, Vec, Vec) { +impl LoadOrder { + fn from(dll_files: &[PathBuf], parsed_order_val: &HashMap) -> Self { + let mut order = LoadOrder::default(); + if dll_files.is_empty() { + return order; + } + if let Some(files) = dll_files + .iter() + .map(|f| { + let file_name = f.file_name(); + Some(file_name?.to_string_lossy().replace(OFF_STATE, "")) + }) + .collect::>>() + { + for (i, dll) in files.iter().enumerate() { + if let Some(v) = parsed_order_val.get(dll) { + order.set = true; + order.i = i; + order.at = *v; + break; + } + } + } else { + error!("Failed to retrieve file_name for Path in: {dll_files:?} Returning LoadOrder::default") + }; + order + } +} + +impl SplitFiles { + fn from(in_files: Vec) -> Self { let len = in_files.len(); - let mut mod_files = Vec::with_capacity(len); - let mut config_files = Vec::with_capacity(len); - let mut other_files = Vec::with_capacity(len); + let mut dll = Vec::with_capacity(len); + let mut config = Vec::with_capacity(len); + let mut other = Vec::with_capacity(len); in_files.into_iter().for_each(|file| { match FileData::from(&file.to_string_lossy()).extension { - ".dll" => mod_files.push(file), - ".ini" => config_files.push(file), - _ => other_files.push(file), + ".dll" => dll.push(file), + ".ini" => config.push(file), + _ => other.push(file), } }); - (mod_files, config_files, other_files) + SplitFiles { dll, config, other } + } + + /// returns references to all files + pub fn file_refs(&self) -> Vec<&Path> { + let mut path_refs = Vec::with_capacity(self.len()); + path_refs.extend(self.dll.iter().map(|f| f.as_path())); + path_refs.extend(self.config.iter().map(|f| f.as_path())); + path_refs.extend(self.other.iter().map(|f| f.as_path())); + path_refs + } + + /// returns references to `input_files` + `self.config` + `self.other` + pub fn add_other_files_to_files<'a>(&'a self, files: &'a [PathBuf]) -> Vec<&'a Path> { + let mut path_refs = Vec::with_capacity(files.len() + self.other_files_len()); + path_refs.extend(files.iter().map(|f| f.as_path())); + path_refs.extend(self.config.iter().map(|f| f.as_path())); + path_refs.extend(self.other.iter().map(|f| f.as_path())); + path_refs + } + + #[inline] + /// total number of files + pub fn len(&self) -> usize { + self.dll.len() + self.config.len() + self.other.len() + } + + #[inline] + /// returns true if all fields contain no PathBufs + pub fn is_empty(&self) -> bool { + self.dll.is_empty() && self.config.is_empty() && self.other.is_empty() + } + + #[inline] + /// number of `config` and `other` + pub fn other_files_len(&self) -> usize { + self.config.len() + self.other.len() } +} - /// This function omits the population of the `order` field +impl RegMod { + /// this function omits the population of the `order` field pub fn new(name: &str, state: bool, in_files: Vec) -> Self { - let (mod_files, config_files, other_files) = RegMod::split_out_config_files(in_files); RegMod { name: String::from(name), state, - mod_files, - config_files, - other_files, + files: SplitFiles::from(in_files), order: LoadOrder::default(), } } - /// This function populates all fields - fn new_full( + /// unlike `new` this function returns a `RegMod` with all fields populated + /// `parsed_order_val` can be obtained from `ModLoaderCfg::parse_section()` + pub fn with_load_order( name: &str, state: bool, in_files: Vec, - parsed_order_val: &mut Vec<(String, usize)>, - ) -> std::io::Result { - let (mod_files, config_files, other_files) = RegMod::split_out_config_files(in_files); - let mut order = LoadOrder::default(); - let dll_files = mod_files - .iter() - .map(|f| { - let file_name = f.file_name().ok_or(String::from("Bad file name")); - Ok(file_name?.to_string_lossy().replace(OFF_STATE, "")) - }) - .collect::, String>>() - .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?; - for (i, dll) in dll_files.iter().enumerate() { - if let Some(remove_i) = parsed_order_val.iter().position(|(k, _)| k == dll) { - order.set = true; - order.i = i; - order.at = parsed_order_val.swap_remove(remove_i).1; - break; - } + parsed_order_val: &HashMap, + ) -> Self { + let split_files = SplitFiles::from(in_files); + let load_order = LoadOrder::from(&split_files.dll, parsed_order_val); + RegMod { + name: String::from(name), + state, + files: split_files, + order: load_order, } - Ok(RegMod { + } + + fn from_split_files(name: &str, state: bool, in_files: SplitFiles, order: LoadOrder) -> Self { + RegMod { name: String::from(name), state, - mod_files, - config_files, - other_files, + files: in_files, order, - }) + } } pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { - type ModData<'a> = Vec<(&'a str, Result, Vec)>; - - fn sync_keys<'a>(ini: &'a Ini, ini_path: &Path) -> std::io::Result> { - fn collect_file_data(section: &Properties) -> HashMap<&str, Vec<&str>> { + type CollectedMaps<'a> = (HashMap<&'a str, &'a str>, HashMap<&'a str, Vec<&'a str>>); + type ModData<'a> = Vec<( + &'a str, + Result, + SplitFiles, + LoadOrder, + )>; + + fn sync_keys<'a>(ini: &'a Ini, ini_path: &Path) -> std::io::Result> { + fn collect_paths(section: &Properties) -> HashMap<&str, Vec<&str>> { section .iter() .enumerate() @@ -396,34 +428,14 @@ impl RegMod { .collect() } - fn combine_map_data<'a>( - state_map: HashMap<&'a str, &str>, - file_map: HashMap<&str, Vec<&str>>, - ) -> ModData<'a> { - let mut mod_data = state_map - .iter() - .filter_map(|(&key, &state_str)| { - file_map.get(&key).map(|file_strs| { - ( - key, - state_str.to_lowercase().parse::(), - file_strs.iter().map(PathBuf::from).collect::>(), - ) - }) - }) - .collect::(); - mod_data.sort_by_key(|(key, _, _)| *key); - mod_data - } - let mod_state_data = ini .section(Some("registered-mods")) .expect("Validated by Ini::is_setup on startup"); - let mod_files_data = ini + let dll_data = ini .section(Some("mod-files")) .expect("Validated by Ini::is_setup on startup"); let mut state_data = mod_state_data.iter().collect::>(); - let mut file_data = collect_file_data(mod_files_data); + let mut file_data = collect_paths(dll_data); let invalid_state = state_data .keys() .filter(|k| !file_data.contains_key(*k)) @@ -452,22 +464,53 @@ impl RegMod { warn!("\"{key}\" has no matching state"); } - Ok(combine_map_data(state_data, file_data)) + Ok((state_data, file_data)) } - fn collect_data_unsafe(ini: &Ini) -> Vec<(&str, &str, Vec<&str>)> { + fn combine_map_data<'a>( + map_data: CollectedMaps<'a>, + parsed_order_val: &HashMap, + ) -> ModData<'a> { + let mut count = 0_usize; + let mut mod_data = map_data + .0 + .iter() + .filter_map(|(&key, &state_str)| { + map_data.1.get(&key).map(|file_strs| { + let split_files = SplitFiles::from( + file_strs.iter().map(PathBuf::from).collect::>(), + ); + let load_order = LoadOrder::from(&split_files.dll, parsed_order_val); + if load_order.set { + count += 1 + } + ( + key, + state_str.to_lowercase().parse::(), + split_files, + load_order, + ) + }) + }) + .collect::(); + mod_data.sort_by_key(|(_, _, _, l)| if l.set { l.at } else { usize::MAX }); + mod_data[count..].sort_by_key(|(key, _, _, _)| *key); + mod_data + } + + fn collect_data_unchecked(ini: &Ini) -> Vec<(&str, &str, Vec<&str>)> { let mod_state_data = ini .section(Some("registered-mods")) .expect("Validated by Ini::is_setup on startup"); - let mod_files_data = ini + let dll_data = ini .section(Some("mod-files")) .expect("Validated by Ini::is_setup on startup"); - mod_files_data + dll_data .iter() .enumerate() .filter(|(_, (k, _))| *k != "array[]") .map(|(i, (k, v))| { - let paths = mod_files_data + let paths = dll_data .iter() .skip(i + 1) .take_while(|(k, _)| *k == "array[]") @@ -482,7 +525,7 @@ impl RegMod { let ini = get_cfg(ini_path)?; if skip_validation { - let parsed_data = collect_data_unsafe(&ini); + let parsed_data = collect_data_unchecked(&ini); Ok(parsed_data .iter() .map(|(n, s, f)| { @@ -495,66 +538,38 @@ impl RegMod { .collect()) } else { let parsed_data = sync_keys(&ini, ini_path)?; - let game_dir = IniProperty::::read(&ini, Some("paths"), "game_dir", false) - .ok_or(std::io::Error::new( - ErrorKind::InvalidData, - "Could not read \"game_dir\" from file", - ))? - .value; - let mut load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) + let game_dir = + IniProperty::::read(&ini, Some("paths"), "game_dir", false)?.value; + let load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))? .parse_section() .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?; - Ok(parsed_data - .iter() - .filter_map(|(k, s, f)| match &s { - Ok(bool) => match f.len() { - 0 => unreachable!(), - 1 => { - match f[0] - .to_owned() - .validate(&ini, Some("mod-files"), skip_validation) - { - Ok(path) => { - RegMod::new_full(k, *bool, vec![path], &mut load_order_parsed) - .ok() - } - Err(err) => { - error!("Error: {err}"); - remove_entry(ini_path, Some("registered-mods"), k) - .expect("Key is valid"); - None - } - } + let parsed_data = combine_map_data(parsed_data, &load_order_parsed); + let mut output = Vec::with_capacity(parsed_data.len()); + for (k, s, f, l) in parsed_data { + match &s { + Ok(bool) => { + if let Err(err) = f.file_refs().validate(&ini, Some("mod-files")) { + error!("Error: {err}"); + remove_entry(ini_path, Some("registered-mods"), k) + .expect("Key is valid"); + } else { + output.push(RegMod::from_split_files(k, *bool, f, l)) } - 2.. => match f - .to_owned() - .validate(&ini, Some("mod-files"), skip_validation) - { - Ok(paths) => { - RegMod::new_full(k, *bool, paths, &mut load_order_parsed).ok() - } - Err(err) => { - error!("Error: {err}"); - remove_entry(ini_path, Some("registered-mods"), k) - .expect("Key is valid"); - None - } - }, - }, + } Err(err) => { error!("Error: {err}"); remove_entry(ini_path, Some("registered-mods"), k).expect("Key is valid"); - None } - }) - .collect()) + } + } + Ok(output) } } pub fn verify_state(&self, game_dir: &Path, ini_path: &Path) -> std::io::Result<()> { - if (!self.state && self.mod_files.iter().any(FileData::is_enabled)) - || (self.state && self.mod_files.iter().any(FileData::is_disabled)) + if (!self.state && self.files.dll.iter().any(FileData::is_enabled)) + || (self.state && self.files.dll.iter().any(FileData::is_disabled)) { warn!( "wrong file state for \"{}\" chaning file extentions", @@ -564,35 +579,13 @@ impl RegMod { } Ok(()) } - - pub fn file_refs(&self) -> Vec<&Path> { - let mut path_refs = Vec::with_capacity(self.all_files_len()); - path_refs.extend(self.mod_files.iter().map(|f| f.as_path())); - path_refs.extend(self.config_files.iter().map(|f| f.as_path())); - path_refs.extend(self.other_files.iter().map(|f| f.as_path())); - path_refs - } - - pub fn add_other_files_to_files<'a>(&'a self, files: &'a [PathBuf]) -> Vec<&'a Path> { - let mut path_refs = Vec::with_capacity(files.len() + self.other_files_len()); - path_refs.extend(files.iter().map(|f| f.as_path())); - path_refs.extend(self.config_files.iter().map(|f| f.as_path())); - path_refs.extend(self.other_files.iter().map(|f| f.as_path())); - path_refs - } - - pub fn all_files_len(&self) -> usize { - self.mod_files.len() + self.config_files.len() + self.other_files.len() - } - - pub fn other_files_len(&self) -> usize { - self.config_files.len() + self.other_files.len() - } } + pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { files.iter().any(|path| { mod_data.iter().any(|registered_mod| { registered_mod + .files .file_refs() .iter() .any(|mod_file| path == mod_file) @@ -613,12 +606,34 @@ impl IntoIoError for ini::Error { } } +impl IntoIoError for std::str::ParseBoolError { + fn into_io_error(self) -> std::io::Error { + std::io::Error::new(ErrorKind::InvalidData, self.to_string()) + } +} + +impl IntoIoError for std::num::ParseIntError { + fn into_io_error(self) -> std::io::Error { + std::io::Error::new(ErrorKind::InvalidData, self.to_string()) + } +} + +pub trait ModError { + fn add_msg(self, msg: String) -> std::io::Error; +} + +impl ModError for std::io::Error { + fn add_msg(self, msg: String) -> std::io::Error { + std::io::Error::new(self.kind(), format!("{msg}\n\nError: {self}")) + } +} + pub trait ErrorClone { - fn clone_err(&self) -> std::io::Error; + fn clone_err(self) -> std::io::Error; } -impl ErrorClone for std::io::Error { - fn clone_err(&self) -> std::io::Error { +impl ErrorClone for &std::io::Error { + fn clone_err(self) -> std::io::Error { std::io::Error::new(self.kind(), self.to_string()) } } diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 2d7c285..a7238f4 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -135,9 +135,9 @@ mod tests { .unwrap(); // Tests if PathBuf and Vec's from Section("mod-files") parse correctly | these are partial paths - assert_eq!(mod_1[0], reg_mod_1.mod_files[0]); - assert_eq!(mod_1[1], reg_mod_1.config_files[0]); - assert_eq!(mod_2, reg_mod_2.mod_files[0]); + assert_eq!(mod_1[0], reg_mod_1.files.dll[0]); + assert_eq!(mod_1[1], reg_mod_1.files.config[0]); + assert_eq!(mod_2, reg_mod_2.files.dll[0]); // Tests if bool was parsed correctly assert_eq!(mod_1_state, reg_mod_1.state); diff --git a/tests/test_lib.rs b/tests/test_lib.rs index db497bc..148e324 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -36,14 +36,15 @@ mod tests { let test_mod = RegMod::new("Test", true, test_files.clone()); let mut test_files_disabled = test_mod - .mod_files + .files + .dll .iter() .map(|file| PathBuf::from(format!("{}{OFF_STATE}", file.display()))) .collect::>(); - assert_eq!(test_mod.mod_files.len(), 1); - assert_eq!(test_mod.config_files.len(), 1); - assert_eq!(test_mod.other_files.len(), 4); + assert_eq!(test_mod.files.dll.len(), 1); + assert_eq!(test_mod.files.config.len(), 1); + assert_eq!(test_mod.files.other.len(), 4); for test_file in test_files.iter() { File::create(test_file.to_string_lossy().to_string()).unwrap(); @@ -61,8 +62,8 @@ mod tests { assert!(file_exists(path_to_test.as_path())); } - test_files_disabled.extend(test_mod.config_files); - test_files_disabled.extend(test_mod.other_files); + test_files_disabled.extend(test_mod.files.config); + test_files_disabled.extend(test_mod.files.other); let test_mod = RegMod::new(&test_mod.name, false, test_files_disabled); toggle_files( From 1e099f5148433d5d4a538ae5ab3eda58cc8ecccf Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 1 May 2024 02:27:19 -0500 Subject: [PATCH 36/62] changed interface for does_dir_contain more changes to come for the interface, now uses a HashSet internally ModLoaderCfg::parse() now uses a HashMap --- src/lib.rs | 99 ++++++++++++++++++++++++++----------- src/main.rs | 41 +++++++-------- src/utils/ini/mod_loader.rs | 69 ++++++++++++++++---------- src/utils/installer.rs | 8 +-- 4 files changed, 136 insertions(+), 81 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 080ed99..5492fe3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ use utils::ini::{ }; use std::{ + collections::HashSet, io::ErrorKind, path::{Path, PathBuf}, }; @@ -27,6 +28,7 @@ const DEFAULT_GAME_DIR: [&str; 6] = [ "ELDEN RING", "Game", ]; + pub const REQUIRED_GAME_FILES: [&str; 3] = [ "eldenring.exe", "oo2core_6_win64.dll", @@ -102,7 +104,7 @@ pub fn toggle_files( } else if !new_state { new_name.push_str(OFF_STATE); } - let mut new_path = path.clone(); + let mut new_path = PathBuf::from(path); new_path.set_file_name(new_name); new_path }) @@ -147,10 +149,10 @@ pub fn toggle_files( save_bool(save_file, Some("registered-mods"), key, state)?; Ok(()) } - let num_rename_files = reg_mod.mod_files.len(); - let num_total_files = num_rename_files + reg_mod.other_files_len(); + let num_rename_files = reg_mod.files.dll.len(); + let num_total_files = num_rename_files + reg_mod.files.other_files_len(); - let file_paths = std::sync::Arc::new(reg_mod.mod_files.clone()); + let file_paths = std::sync::Arc::new(reg_mod.files.dll.clone()); let file_paths_clone = file_paths.clone(); let game_dir_clone = game_dir.to_path_buf(); @@ -160,8 +162,8 @@ pub fn toggle_files( std::thread::spawn(move || join_paths(&game_dir_clone, &file_paths_clone)); let short_path_new = new_short_paths_thread.join().unwrap_or(Vec::new()); - let all_short_paths = reg_mod.add_other_files_to_files(&short_path_new); - let full_path_new = join_paths(Path::new(game_dir), &short_path_new); + let all_short_paths = reg_mod.files.add_other_files_to_files(&short_path_new); + let full_path_new = join_paths(game_dir, &short_path_new); let full_path_original = original_full_paths_thread.join().unwrap_or(Vec::new()); rename_files(&num_rename_files, &full_path_original, &full_path_new)?; @@ -187,21 +189,51 @@ pub enum Operation { Any, } -pub fn does_dir_contain(path: &Path, operation: Operation, list: &[&str]) -> std::io::Result { +#[derive(Default)] +pub struct OperationResult { + pub success: bool, + pub files_found: usize, +} + +pub fn does_dir_contain( + path: &Path, + operation: Operation, + list: &[&str], +) -> std::io::Result { let entries = std::fs::read_dir(path)?; let file_names = entries - .map(|entry| Ok(entry?.file_name())) - .collect::>>()?; + // MARK: FIXME + // would be nice if we could leave as a OsString here + // change count to be the actual file found + // can we make a cleaner interface? + // not force to match against all file situations? use enum to return different data types? + // use bool for called to decide if they want to match against extra data? + .map(|entry| Ok(entry?.file_name().to_string_lossy().to_string())) + .collect::>>()?; - let result = match operation { - Operation::All => list - .iter() - .all(|check_file| file_names.iter().any(|file_name| file_name == check_file)), - Operation::Any => list - .iter() - .any(|check_file| file_names.iter().any(|file_name| file_name == check_file)), - }; - Ok(result) + match operation { + Operation::All => { + let mut count = 0_usize; + list.iter().for_each(|&check_file| { + if file_names.contains(check_file) { + count += 1 + } + }); + Ok(OperationResult { + success: count == list.len(), + files_found: count, + }) + } + Operation::Any => { + let result = list + .iter() + .any(|&check_file| file_names.contains(check_file)); + Ok(OperationResult { + success: result, + files_found: if result { 1 } else { 0 }, + }) + } + } } pub struct FileData<'a> { pub name: &'a str, @@ -294,23 +326,27 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { return Ok(PathResult::None(PathBuf::new())); } }; - if let Some(path) = IniProperty::::read(&config, Some("paths"), "game_dir", false) + if let Ok(path) = IniProperty::::read(&config, Some("paths"), "game_dir", false) .and_then(|ini_property| { match does_dir_contain(&ini_property.value, Operation::All, &REQUIRED_GAME_FILES) { - Ok(true) => Some(ini_property.value), - Ok(false) => { - error!( - "{}", - format!( - "Required Game files not found in:\n\"{}\"", - ini_property.value.display() - ) + Ok(OperationResult { + success: true, + files_found: _, + }) => Ok(ini_property.value), + Ok(OperationResult { + success: false, + files_found: _, + }) => { + let err = format!( + "Required Game files not found in:\n\"{}\"", + ini_property.value.display() ); - None + error!("{err}",); + new_io_error!(ErrorKind::NotFound, err) } Err(err) => { error!("Error: {err}"); - None + Err(err) } } }) @@ -319,7 +355,10 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { return Ok(PathResult::Full(path)); } let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); - if does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES).unwrap_or(false) { + if does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES) + .unwrap_or_default() + .success + { info!("Success: located \"game_dir\" on drive"); save_path(file_name, Some("paths"), "game_dir", try_locate.as_path())?; return Ok(PathResult::Full(try_locate)); diff --git a/src/main.rs b/src/main.rs index ab1fa61..0fff23f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -123,8 +123,10 @@ fn main() -> Result<(), slint::PlatformError> { "dark_mode", false, ) { - Some(bool) => ui.global::().set_dark_mode(bool.value), - None => { + Ok(bool) => ui.global::().set_dark_mode(bool.value), + Err(err) => { + // io::Read error + errors.push(err); ui.global::().set_dark_mode(true); save_bool(current_ini, Some("app-settings"), "dark_mode", true) // io::Write error @@ -409,8 +411,8 @@ fn main() -> Result<(), slint::PlatformError> { info!("User Selected Path: \"{}\"", path.display()); let try_path: PathBuf = match does_dir_contain(&path, Operation::All, &["Game"]) { - Ok(true) => PathBuf::from(&format!("{}\\Game", path.display())), - Ok(false) => path, + Ok(OperationResult { success: true, files_found: _ }) => PathBuf::from(&format!("{}\\Game", path.display())), + Ok(OperationResult { success: false, files_found: _ }) => path, Err(err) => { error!("{err}"); ui.display_msg(&err.to_string()); @@ -418,7 +420,7 @@ fn main() -> Result<(), slint::PlatformError> { } }; match does_dir_contain(Path::new(&try_path), Operation::All, &REQUIRED_GAME_FILES) { - Ok(true) => { + Ok(OperationResult { success: true, files_found: _ }) => { let result = save_path(current_ini, Some("paths"), "game_dir", &try_path); if result.is_err() && save_path(current_ini, Some("paths"), "game_dir", &try_path).is_err() { let err = result.unwrap_err(); @@ -441,7 +443,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") } } - Ok(false) => { + Ok(OperationResult { success: false, files_found: _ }) => { let err = format!("Required Game files not found in:\n\"{}\"", try_path.display()); error!("{err}"); ui.display_msg(&err); @@ -567,11 +569,11 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg("A selected file is already registered to a mod"); } else { let num_files = files.len(); - let mut new_data = found_mod.mod_files.clone(); + let mut new_data = found_mod.files.dll.clone(); new_data.extend(files); let mut results = Vec::with_capacity(3); - let new_data_refs = found_mod.add_other_files_to_files(&new_data); - if found_mod.all_files_len() == 1 { + let new_data_refs = found_mod.files.add_other_files_to_files(&new_data); + if found_mod.files.len() == 1 { results.push(remove_entry( current_ini, Some("mod-files"), @@ -654,7 +656,7 @@ fn main() -> Result<(), slint::PlatformError> { if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { - let mut found_files = found_mod.mod_files.clone(); + let mut found_files = found_mod.files.dll.clone(); if found_files.iter().any(FileData::is_disabled) { match toggle_files(&game_dir, true, found_mod, Some(current_ini)) { Ok(files) => found_files = files, @@ -667,7 +669,7 @@ fn main() -> Result<(), slint::PlatformError> { // we can let sync keys take care of removing files from ini remove_entry(current_ini, Some("registered-mods"), &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); - let file_refs = found_mod.add_other_files_to_files(&found_files); + let file_refs = found_mod.files.add_other_files_to_files(&found_files); let ui_handle = ui.as_weak(); match confirm_remove_mod(ui_handle, &game_dir, file_refs).await { Ok(_) => ui.display_msg(&format!("Successfully removed all files associated with the previously registered mod \"{key}\"")), @@ -1072,16 +1074,16 @@ fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { let files: Rc> = Default::default(); let dll_files: Rc> = Default::default(); let config_files: Rc> = Default::default(); - if !mod_data.mod_files.is_empty() { - files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); - dll_files.extend(mod_data.mod_files.iter().map(|f| SharedString::from(f.file_name().unwrap().to_string_lossy().replace(OFF_STATE, "")))); + if !mod_data.files.dll.is_empty() { + files.extend(mod_data.files.dll.iter().map(|f| SharedString::from(f.to_string_lossy().replace(OFF_STATE, "")).into())); + dll_files.extend(mod_data.files.dll.iter().map(|f| SharedString::from(f.file_name().unwrap().to_string_lossy().replace(OFF_STATE, "")))); }; - if !mod_data.config_files.is_empty() { - files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); - config_files.extend(mod_data.config_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()))); + if !mod_data.files.config.is_empty() { + files.extend(mod_data.files.config.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); + config_files.extend(mod_data.files.config.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()))); }; - if !mod_data.other_files.is_empty() { - files.extend(mod_data.other_files.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); + if !mod_data.files.other.is_empty() { + files.extend(mod_data.files.other.iter().map(|f| SharedString::from(f.to_string_lossy().to_string()).into())); }; let name = mod_data.name.replace('_', " "); display_mods.push(DisplayMod { @@ -1103,7 +1105,6 @@ fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { } // MARK: TODO -// need to be able to sort RegMods by load-order then albethabetical // need to use ModelNotify::row_changed to handle updating page info on change // ui.invoke_update_mod_index(1, 1); diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index de42778..fd9c042 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -1,6 +1,7 @@ use ini::Ini; use log::{error, info, trace}; use std::{ + collections::HashMap, io::ErrorKind, path::{Path, PathBuf}, }; @@ -8,10 +9,10 @@ use std::{ use crate::{ new_io_error, utils::ini::{ - parser::{IniProperty, RegMod}, + parser::{IniProperty, ModError, RegMod}, writer::EXT_OPTIONS, }, - LOADER_KEYS, LOADER_SECTIONS, + OperationResult, LOADER_KEYS, LOADER_SECTIONS, {does_dir_contain, get_cfg, Operation, LOADER_FILES, LOADER_FILES_DISABLED}, }; @@ -25,7 +26,12 @@ pub struct ModLoader { impl ModLoader { pub fn properties(game_dir: &Path) -> ModLoader { match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { - Ok(true) => { + // MARK: IMPL FEAT? + // add branch for if ini not found then create ini with default values + Ok(OperationResult { + success: true, + files_found: _, + }) => { info!("Found mod loader files"); ModLoader { installed: true, @@ -33,10 +39,16 @@ impl ModLoader { path: game_dir.join(LOADER_FILES[0]), } } - Ok(false) => { + Ok(OperationResult { + success: false, + files_found: _, + }) => { trace!("Checking if mod loader is disabled"); match does_dir_contain(game_dir, Operation::All, &LOADER_FILES_DISABLED) { - Ok(true) => { + Ok(OperationResult { + success: true, + files_found: _, + }) => { info!("Found mod loader files in the disabled state"); ModLoader { installed: true, @@ -44,7 +56,10 @@ impl ModLoader { path: game_dir.join(LOADER_FILES[0]), } } - Ok(false) => { + Ok(OperationResult { + success: false, + files_found: _, + }) => { error!("Mod Loader Files not found in selected path"); ModLoader::default() } @@ -90,8 +105,14 @@ impl ModLoaderCfg { return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); } let cfg_dir = match does_dir_contain(game_dir, Operation::All, &[LOADER_FILES[0]]) { - Ok(true) => game_dir.join(LOADER_FILES[0]), - Ok(false) => { + Ok(OperationResult { + success: true, + files_found: _, + }) => game_dir.join(LOADER_FILES[0]), + Ok(OperationResult { + success: false, + files_found: _, + }) => { return new_io_error!( ErrorKind::NotFound, "\"mod_loader_config.ini\" does not exist in the current game_dir" @@ -142,27 +163,21 @@ impl ModLoaderCfg { pub fn get_load_delay(&self) -> std::io::Result { match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], false) { - Some(delay_time) => Ok(delay_time.value), - None => new_io_error!( - ErrorKind::InvalidData, - format!( - "Found an unexpected character saved in \"{}\"", - LOADER_KEYS[0] - ) - ), + Ok(delay_time) => Ok(delay_time.value), + Err(err) => Err(err.add_msg(format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[0] + ))), } } pub fn get_show_terminal(&self) -> std::io::Result { match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1], false) { - Some(delay_time) => Ok(delay_time.value), - None => new_io_error!( - ErrorKind::InvalidData, - format!( - "Found an unexpected character saved in \"{}\"", - LOADER_KEYS[1] - ) - ), + Ok(delay_time) => Ok(delay_time.value), + Err(err) => Err(err.add_msg(format!( + "Found an unexpected character saved in \"{}\"", + LOADER_KEYS[1] + ))), } } @@ -181,14 +196,14 @@ impl ModLoaderCfg { self.section().iter() } - /// Returns an owned `Vec` with values parsed into `usize` - pub fn parse_section(&self) -> Result, std::num::ParseIntError> { + /// Returns an owned `HashMap` with values parsed into K: `String`, V: `usize` + pub fn parse_section(&self) -> Result, std::num::ParseIntError> { self.iter() .map(|(k, v)| { let parse_v = v.parse::(); Ok((k.to_string(), parse_v?)) }) - .collect::, _>>() + .collect::, _>>() } #[inline] diff --git a/src/utils/installer.rs b/src/utils/installer.rs index ee6fe11..ea53f49 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -216,8 +216,8 @@ impl InstallData { file_paths: Vec, game_dir: &Path, ) -> std::io::Result { - let amend_mod_split_file_names = amend_to.mod_files.iter().try_fold( - Vec::with_capacity(amend_to.mod_files.len()), + let amend_mod_split_file_names = amend_to.files.dll.iter().try_fold( + Vec::with_capacity(amend_to.files.len()), |mut acc, file| { let file_name = file_name_or_err(file)?.to_string_lossy(); let file_data = FileData::from(&file_name); @@ -398,7 +398,7 @@ impl InstallData { let game_dir = self_clone.install_dir.parent().expect("has parent"); if valid_dir.strip_prefix(game_dir).is_ok() { return new_io_error!(ErrorKind::InvalidInput, "Files are already installed"); - } else if does_dir_contain(&valid_dir, crate::Operation::All, &["mods"])? { + } else if does_dir_contain(&valid_dir, crate::Operation::All, &["mods"])?.success { return new_io_error!(ErrorKind::InvalidData, "Invalid file structure"); } @@ -567,7 +567,7 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result &mod_data.name, mod_data.state, )?; - let file_refs = mod_data.file_refs(); + let file_refs = mod_data.files.file_refs(); if file_refs.len() == 1 { save_path(ini_file, Some("mod-files"), &mod_data.name, file_refs[0])?; } else { From 65f9d434eefbfa68c79f58d2f9e6ebc958c4e470 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 1 May 2024 14:54:57 -0500 Subject: [PATCH 37/62] improvements to parsing logic reduced calls to read game_dir when verifiying Vec now uses generics over AsRef and [AsRef] for caller conveinence. added type checks so you get the correct error message if you input the wrong type to parse into between Vec and PathBuf --- src/utils/ini/parser.rs | 164 +++++++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 59 deletions(-) diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index a36d89c..60feee8 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -2,6 +2,7 @@ use ini::{Ini, Properties}; use log::{error, warn}; use std::{ collections::HashMap, + fmt::Debug, io::ErrorKind, path::{Path, PathBuf}, }; @@ -16,18 +17,20 @@ use crate::{ }; pub trait Parsable: Sized { - fn parse_str( + fn parse_str>( ini: &Ini, section: Option<&str>, + partial_path: Option, key: &str, skip_validation: bool, ) -> std::io::Result; } impl Parsable for bool { - fn parse_str( + fn parse_str>( ini: &Ini, section: Option<&str>, + _partial_path: Option, key: &str, _skip_validation: bool, ) -> std::io::Result { @@ -46,9 +49,10 @@ impl Parsable for bool { } impl Parsable for u32 { - fn parse_str( + fn parse_str>( ini: &Ini, section: Option<&str>, + _partial_path: Option, key: &str, _skip_validation: bool, ) -> std::io::Result { @@ -60,30 +64,36 @@ impl Parsable for u32 { } impl Parsable for PathBuf { - fn parse_str( + fn parse_str>( ini: &Ini, section: Option<&str>, + partial_path: Option, key: &str, skip_validation: bool, ) -> std::io::Result { - let parsed_value = PathBuf::from( - ini.get_from(section, key) - .expect("Validated by IniProperty::is_valid"), - ); + let parsed_value = PathBuf::from({ + let value = ini.get_from(section, key); + if matches!(value, Some("array")) { + return new_io_error!( + ErrorKind::InvalidData, + "Invalid type found. Expected: Path, Found: Vec" + ); + } + value.expect("Validated by IniProperty::is_valid") + }); if skip_validation { - Ok(parsed_value) - } else if let Err(err) = parsed_value.as_path().validate(ini, section) { - Err(err) - } else { - Ok(parsed_value) + return Ok(parsed_value); } + parsed_value.as_path().validate(partial_path)?; + Ok(parsed_value) } } impl Parsable for Vec { - fn parse_str( + fn parse_str>( ini: &Ini, section: Option<&str>, + partial_path: Option, key: &str, skip_validation: bool, ) -> std::io::Result { @@ -96,65 +106,65 @@ impl Parsable for Vec { .map(|(_, v)| PathBuf::from(v)) .collect() } - + if !matches!(ini.get_from(section, key), Some("array")) { + return new_io_error!( + ErrorKind::InvalidData, + "Invalid type found. Expected: Vec, Found: Path" + ); + } let parsed_value = read_array( ini.section(section) .expect("Validated by IniProperty::is_valid"), key, ); if skip_validation { - Ok(parsed_value) - } else if let Some(err) = parsed_value - .iter() - .find_map(|f| f.as_path().validate(ini, section).err()) - { - Err(err) - } else { - Ok(parsed_value) + return Ok(parsed_value); } + parsed_value.validate(partial_path)?; + Ok(parsed_value) } } pub trait Valitidity { - fn is_setup(&self) -> bool { - true - } - #[allow(unused_variables)] - fn validate(&self, ini: &Ini, section: Option<&str>) -> std::io::Result<()> { - Ok(()) - } + /// _full_paths_ are assumed to Point to directories, where as _partial_paths_ are assumed to point to files + /// if you want to validate a _partial_path_ you must supply the _path_prefix_ + fn validate>(&self, partial_path: Option

) -> std::io::Result<()>; } -impl Valitidity for Ini { - fn is_setup(&self) -> bool { - INI_SECTIONS.iter().all(|section| { - let trimmed_section: String = section.trim_matches(|c| c == '[' || c == ']').to_owned(); - self.section(Some(trimmed_section)).is_some() - }) - } -} - -impl Valitidity for &Path { - fn validate(&self, ini: &Ini, section: Option<&str>) -> std::io::Result<()> { - if section == Some("mod-files") { - let game_dir = - IniProperty::::read(ini, Some("paths"), "game_dir", false)?.value; - validate_file(&game_dir.join(self))?; +impl> Valitidity for T { + fn validate>(&self, partial_path: Option

) -> std::io::Result<()> { + if let Some(prefix) = partial_path { + validate_file(&prefix.as_ref().join(self))?; Ok(()) } else { - validate_existance(self)?; + validate_existance(self.as_ref())?; Ok(()) } } } -impl Valitidity for Vec<&Path> { - fn validate(&self, ini: &Ini, section: Option<&str>) -> std::io::Result<()> { - if let Some(err) = self.iter().find_map(|f| f.validate(ini, section).err()) { - Err(err) - } else { - Ok(()) +impl> Valitidity for [T] { + fn validate>(&self, partial_path: Option

) -> std::io::Result<()> { + let mut add_errors = String::new(); + let mut init_err = std::io::Error::new(ErrorKind::WriteZero, ""); + self.iter().for_each(|f| { + if let Err(err) = f.validate(partial_path.as_ref()) { + if init_err.kind() == ErrorKind::WriteZero { + init_err = err; + } else if add_errors.is_empty() { + add_errors = err.to_string() + } else { + add_errors.push_str(&format!("\n{err}")) + } + } + }); + if init_err.kind() != ErrorKind::WriteZero { + if add_errors.is_empty() { + return Err(init_err); + } + return Err(init_err.add_msg(add_errors)); } + Ok(()) } } @@ -162,13 +172,14 @@ fn validate_file(path: &Path) -> std::io::Result<()> { if path.extension().is_none() { let input_file = path.to_string_lossy().to_string(); let split = input_file.rfind('\\').unwrap_or(0); - input_file - .split_at(if split != 0 { split + 1 } else { split }) - .1 - .to_string(); return new_io_error!( ErrorKind::InvalidInput, - format!("\"{input_file}\" does not have an extention") + format!( + "\"{}\" does not have an extention", + input_file + .split_at(if split != 0 { split + 1 } else { split }) + .1 + ) ); } validate_existance(path) @@ -193,6 +204,22 @@ fn validate_existance(path: &Path) -> std::io::Result<()> { } } +pub trait Setup { + fn is_setup(&self) -> bool; +} + +impl Setup for Ini { + // MARK: FIXME + // add functionality for matching Ini filename + fn is_setup(&self) -> bool { + INI_SECTIONS.iter().all(|section| { + self.section(Some(section.trim_matches(|c| c == '[' || c == ']'))) + .is_some() + }) + } +} + +#[derive(Debug)] pub struct IniProperty { //section: Option, //key: String, @@ -221,7 +248,18 @@ impl IniProperty { ) -> std::io::Result { match &ini.section(section) { Some(s) => match s.contains_key(key) { - true => T::parse_str(ini, section, key, skip_validation), + true => { + // This will have to be abstracted to the caller if we want the ability for the caller to specify the _path_prefix_ + // right now _game_dir_ is the only valid prefix && "mod-files" is the only place _short_paths_ are stored + let game_dir = match section { + Some("mod-files") => Some( + IniProperty::::read(ini, Some("paths"), "game_dir", false)? + .value, + ), + _ => None, + }; + T::parse_str(ini, section, game_dir, key, skip_validation) + } false => new_io_error!( ErrorKind::NotFound, format!("Key: \"{key}\" not found in {ini:?}") @@ -401,6 +439,9 @@ impl RegMod { } } + // MARK: FIXME? + // when is the best time to verify parsed data? currently we verify data after shaping it + // the code would most likely be cleaner if we verified it apon parsing before doing any shaping pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { type CollectedMaps<'a> = (HashMap<&'a str, &'a str>, HashMap<&'a str, Vec<&'a str>>); type ModData<'a> = Vec<( @@ -464,6 +505,7 @@ impl RegMod { warn!("\"{key}\" has no matching state"); } + assert_eq!(state_data.len(), file_data.len()); Ok((state_data, file_data)) } @@ -493,6 +535,10 @@ impl RegMod { }) }) .collect::(); + + // if this fails `sync_keys()` did not do its job + assert_eq!(map_data.1.len(), mod_data.len()); + mod_data.sort_by_key(|(_, _, _, l)| if l.set { l.at } else { usize::MAX }); mod_data[count..].sort_by_key(|(key, _, _, _)| *key); mod_data @@ -549,7 +595,7 @@ impl RegMod { for (k, s, f, l) in parsed_data { match &s { Ok(bool) => { - if let Err(err) = f.file_refs().validate(&ini, Some("mod-files")) { + if let Err(err) = f.file_refs().validate(Some(&game_dir)) { error!("Error: {err}"); remove_entry(ini_path, Some("registered-mods"), k) .expect("Key is valid"); From 742bd8669fa1fe0c5ffb668f87e16dcefd3184db Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 1 May 2024 14:58:09 -0500 Subject: [PATCH 38/62] Updated test cases now tests to make sure type checks work. and bools can be parsed from "1" and "0" --- benches/data_collection_benchmark.rs | 2 +- src/lib.rs | 4 +- src/main.rs | 6 +- src/utils/ini/writer.rs | 2 +- src/utils/installer.rs | 4 +- tests/test_ini_tools.rs | 84 ++++++++++++++++++++++++++-- 6 files changed, 88 insertions(+), 14 deletions(-) diff --git a/benches/data_collection_benchmark.rs b/benches/data_collection_benchmark.rs index ec64364..6430684 100644 --- a/benches/data_collection_benchmark.rs +++ b/benches/data_collection_benchmark.rs @@ -20,7 +20,7 @@ fn populate_non_valid_ini(len: u32, file: &Path) { save_bool(file, Some("registered-mods"), &key, bool_value).unwrap(); if paths.len() > 1 { - save_path_bufs(file, &key, &path_refs).unwrap(); + save_paths(file, &key, &path_refs).unwrap(); } else { save_path(file, Some("mod-files"), &key, paths[0].as_path()).unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index 5492fe3..c0eecf7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,7 @@ use ini::Ini; use log::{error, info, trace, warn}; use utils::ini::{ parser::{IniProperty, IntoIoError, RegMod}, - writer::{remove_array, save_bool, save_path, save_path_bufs}, + writer::{remove_array, save_bool, save_path, save_paths}, }; use std::{ @@ -144,7 +144,7 @@ pub fn toggle_files( save_path(save_file, Some("mod-files"), key, path_to_save[0])?; } else { remove_array(save_file, key)?; - save_path_bufs(save_file, key, path_to_save)?; + save_paths(save_file, key, path_to_save)?; } save_bool(save_file, Some("registered-mods"), key, state)?; Ok(()) diff --git a/src/main.rs b/src/main.rs index 0fff23f..b815e91 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use elden_mod_loader_gui::{ utils::{ ini::{ mod_loader::{ModLoader, ModLoaderCfg, update_order_entries, Countable}, - parser::{file_registered, IniProperty, RegMod, Valitidity, ErrorClone}, + parser::{file_registered, IniProperty, RegMod, Setup, ErrorClone}, writer::*, }, installer::{remove_mod_files, InstallData, scan_for_mods} @@ -349,7 +349,7 @@ fn main() -> Result<(), slint::PlatformError> { )), 2.. => { let path_refs = files.iter().map(|p| p.as_path()).collect::>(); - results.push(save_path_bufs(current_ini, &format_key, &path_refs)) + results.push(save_paths(current_ini, &format_key, &path_refs)) }, } if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { @@ -582,7 +582,7 @@ fn main() -> Result<(), slint::PlatformError> { } else { results.push(remove_array(current_ini, &found_mod.name)); } - results.push(save_path_bufs(current_ini, &found_mod.name, &new_data_refs)); + results.push(save_paths(current_ini, &found_mod.name, &new_data_refs)); if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { ui.display_msg(&err.to_string()); let _ = remove_entry( diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index eb03f72..c6eb9d6 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -27,7 +27,7 @@ pub const EXT_OPTIONS: WriteOption = WriteOption { kv_separator: " = ", }; -pub fn save_path_bufs(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Result<()> { +pub fn save_paths(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Result<()> { let mut config: Ini = get_cfg(file_name)?; let save_paths = files .iter() diff --git a/src/utils/installer.rs b/src/utils/installer.rs index ea53f49..0647faa 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -9,7 +9,7 @@ use crate::{ does_dir_contain, file_name_or_err, new_io_error, parent_or_err, utils::ini::{ parser::RegMod, - writer::{save_bool, save_path, save_path_bufs}, + writer::{save_bool, save_path, save_paths}, }, FileData, }; @@ -571,7 +571,7 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result if file_refs.len() == 1 { save_path(ini_file, Some("mod-files"), &mod_data.name, file_refs[0])?; } else { - save_path_bufs(ini_file, &mod_data.name, &file_refs)?; + save_paths(ini_file, &mod_data.name, &file_refs)?; } mod_data.verify_state(game_dir, ini_file)?; } diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index a7238f4..1f1ea8c 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -8,7 +8,7 @@ mod tests { use elden_mod_loader_gui::{ get_cfg, utils::ini::{ - parser::{IniProperty, RegMod, Valitidity}, + parser::{IniProperty, RegMod, Setup}, writer::*, }, }; @@ -31,9 +31,9 @@ mod tests { let config = get_cfg(test_file).unwrap(); - for (i, _) in test_nums.iter().enumerate() { + for (i, num) in test_nums.iter().enumerate() { assert_eq!( - test_nums[i], + *num, IniProperty::::read(&config, Some("paths"), &format!("test_num_{i}"), false) .unwrap() .value @@ -43,6 +43,37 @@ mod tests { remove_file(test_file).unwrap(); } + #[test] + fn does_bool_parse() { + let test_bools: [&str; 6] = [" True ", "false", "faLSe", "0 ", "0", "1"]; + let bool_results: [bool; 6] = [true, false, false, false, false, true]; + let test_file = Path::new("temp\\test_bools.ini"); + + new_cfg(test_file).unwrap(); + for (i, bool_str) in test_bools.iter().enumerate() { + save_value_ext( + test_file, + Some("paths"), + &format!("test_bool_{i}"), + bool_str, + ) + .unwrap(); + } + + let config = get_cfg(test_file).unwrap(); + + for (i, bool) in bool_results.iter().enumerate() { + assert_eq!( + *bool, + IniProperty::::read(&config, Some("paths"), &format!("test_bool_{i}"), false) + .unwrap() + .value + ) + } + + remove_file(test_file).unwrap(); + } + #[test] fn does_path_parse() { let test_path_1 = @@ -71,6 +102,49 @@ mod tests { remove_file(test_file).unwrap(); } + #[test] + #[allow(unused_variables)] + fn type_check() { + let test_path = + Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); + let test_array = [ + Path::new("mods\\UnlockTheFps.dll"), + Path::new("mods\\UnlockTheFps\\config.ini"), + ]; + let test_file = Path::new("temp\\test_type_check.ini"); + + new_cfg(test_file).unwrap(); + save_path(test_file, Some("paths"), "game_dir", test_path).unwrap(); + save_paths(test_file, "test_array", &test_array).unwrap(); + + let config = get_cfg(test_file).unwrap(); + + let pathbuf_err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid type found. Expected: Path, Found: Vec", + ); + let vec_pathbuf_err = std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Invalid type found. Expected: Vec, Found: Path", + ); + + let vec_result = + IniProperty::>::read(&config, Some("paths"), "game_dir", false); + assert_eq!( + vec_result.unwrap_err().to_string(), + vec_pathbuf_err.to_string() + ); + + let path_result = + IniProperty::::read(&config, Some("mod-files"), "test_array", false); + assert_eq!( + path_result.unwrap_err().to_string(), + pathbuf_err.to_string() + ); + + remove_file(test_file).unwrap(); + } + #[test] fn read_write_delete_from_ini() { let test_file = Path::new("temp\\test_collect_mod_data.ini"); @@ -101,11 +175,11 @@ mod tests { let game_path = Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); - save_path_bufs(test_file, mod_1_key, &mod_1).unwrap(); + save_paths(test_file, mod_1_key, &mod_1).unwrap(); save_bool(test_file, Some("registered-mods"), mod_1_key, mod_1_state).unwrap(); save_path(test_file, Some("mod-files"), mod_2_key, &mod_2).unwrap(); save_bool(test_file, Some("registered-mods"), mod_2_key, mod_2_state).unwrap(); - save_path_bufs(test_file, "no_matching_state_1", &invalid_format_1).unwrap(); + save_paths(test_file, "no_matching_state_1", &invalid_format_1).unwrap(); save_path( test_file, Some("mod-files"), From 855c04194d57b4de2bcc2494f257b4f3e960b5aa Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 1 May 2024 17:22:46 -0500 Subject: [PATCH 39/62] Moved ini sections and keys to const --- Cargo.lock | 58 ++++++------ benches/data_collection_benchmark.rs | 9 +- src/lib.rs | 23 ++++- src/main.rs | 32 +++---- src/utils/ini/parser.rs | 136 ++++++--------------------- src/utils/ini/writer.rs | 6 +- src/utils/installer.rs | 11 +-- tests/test_ini_tools.rs | 49 +++++----- ui/common.slint | 2 + ui/tabs.slint | 3 + 10 files changed, 137 insertions(+), 192 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index efa668b..49db3ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,7 +264,7 @@ dependencies = [ "serde", "serde_repr", "url", - "zbus 4.1.2", + "zbus 4.2.0", ] [[package]] @@ -761,9 +761,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" dependencies = [ "jobserver", "libc", @@ -3604,18 +3604,18 @@ checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "serde" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9f6e76df036c77cd94996771fb40db98187f096dd0b9af39c6c6e452ba966a" +checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.199" +version = "1.0.200" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bd257a6541e141e42ca6d24ae26f7714887b47e89aa739099104c7e4d3b7fc" +checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", @@ -5033,7 +5033,7 @@ dependencies = [ "web-time 0.2.4", "windows-sys 0.48.0", "x11-dl", - "x11rb 0.13.0", + "x11rb 0.13.1", "xkbcommon-dl", ] @@ -5081,7 +5081,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98785a09322d7446e28a13203d2cae1059a0dd3dfb32cb06d0a225f023d8286" dependencies = [ "libc", - "x11rb 0.13.0", + "x11rb 0.13.1", ] [[package]] @@ -5114,9 +5114,9 @@ dependencies = [ [[package]] name = "x11rb" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12" dependencies = [ "as-raw-xcb-connection", "gethostname 0.4.3", @@ -5124,7 +5124,7 @@ dependencies = [ "libloading 0.8.3", "once_cell", "rustix 0.38.34", - "x11rb-protocol 0.13.0", + "x11rb-protocol 0.13.1", ] [[package]] @@ -5138,9 +5138,9 @@ dependencies = [ [[package]] name = "x11rb-protocol" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" +checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" [[package]] name = "xattr" @@ -5254,9 +5254,9 @@ dependencies = [ [[package]] name = "zbus" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9ff46f2a25abd690ed072054733e0bc3157e3d4c45f41bd183dce09c2ff8ab9" +checksum = "6aea58d1af0aaa8abf87f3d9ade9b8f46bf13727e5f9fb24bc31ee9d94a9b4ad" dependencies = [ "async-broadcast 0.7.0", "async-executor", @@ -5268,7 +5268,6 @@ dependencies = [ "async-task", "async-trait", "blocking", - "derivative", "enumflags2", "event-listener 5.3.0", "futures-core", @@ -5286,9 +5285,9 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros 4.1.2", + "zbus_macros 4.2.0", "zbus_names 3.0.0", - "zvariant 4.0.2", + "zvariant 4.0.3", ] [[package]] @@ -5307,14 +5306,13 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.1.2" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0e3852c93dcdb49c9462afe67a2a468f7bd464150d866e861eaf06208633e0" +checksum = "1bf2b496ec1e2d3c4a7878e351607f7a2bec1e1029b353683dfc28a22999e369" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "regex", "syn 1.0.109", "zvariant_utils", ] @@ -5338,7 +5336,7 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant 4.0.2", + "zvariant 4.0.3", ] [[package]] @@ -5386,16 +5384,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b3ca6db667bfada0f1ebfc94b2b1759ba25472ee5373d4551bb892616389a" +checksum = "4e9282c6945d9e27742ba7ad7191325546636295de7b83f6735af73159b32ac7" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", "url", - "zvariant_derive 4.0.2", + "zvariant_derive 4.0.3", ] [[package]] @@ -5413,9 +5411,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "4.0.2" +version = "4.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4b236063316163b69039f77ce3117accb41a09567fd24c168e43491e521bc" +checksum = "0142549e559746ff09d194dd43d256a554299d286cc56460a082b8ae24652aa1" dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", @@ -5426,9 +5424,9 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bedb16a193cc12451873fee2a1bc6550225acece0e36f333e68326c73c8172" +checksum = "75fa7291bdd68cd13c4f97cc9d78cbf16d96305856dfc7ac942aeff4c2de7d5a" dependencies = [ "proc-macro2", "quote", diff --git a/benches/data_collection_benchmark.rs b/benches/data_collection_benchmark.rs index 6430684..7a33835 100644 --- a/benches/data_collection_benchmark.rs +++ b/benches/data_collection_benchmark.rs @@ -1,6 +1,9 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use elden_mod_loader_gui::utils::ini::{parser::RegMod, writer::*}; +use elden_mod_loader_gui::{ + utils::ini::{parser::RegMod, writer::*}, + INI_SECTIONS, +}; use rand::{distributions::Alphanumeric, Rng}; use std::{ fs::remove_file, @@ -18,11 +21,11 @@ fn populate_non_valid_ini(len: u32, file: &Path) { let paths = generate_test_paths(); let path_refs = paths.iter().map(|p| p.as_path()).collect::>(); - save_bool(file, Some("registered-mods"), &key, bool_value).unwrap(); + save_bool(file, INI_SECTIONS[2], &key, bool_value).unwrap(); if paths.len() > 1 { save_paths(file, &key, &path_refs).unwrap(); } else { - save_path(file, Some("mod-files"), &key, paths[0].as_path()).unwrap(); + save_path(file, INI_SECTIONS[3], &key, paths[0].as_path()).unwrap(); } } } diff --git a/src/lib.rs b/src/lib.rs index c0eecf7..b77f789 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,16 @@ pub const REQUIRED_GAME_FILES: [&str; 3] = [ ]; pub const OFF_STATE: &str = ".disabled"; +pub const INI_SECTIONS: [Option<&str>; 4] = [ + Some("app-settings"), + Some("paths"), + Some("registered-mods"), + Some("mod-files"), +]; +pub const INI_KEYS: [&str; 2] = ["dark_mode", "game_dir"]; +pub const ARRAY_KEY: &str = "array[]"; +pub const ARRAY_VALUE: &str = "array"; + pub const LOADER_FILES: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll"]; pub const LOADER_FILES_DISABLED: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll.disabled"]; pub const LOADER_SECTIONS: [Option<&str>; 2] = [Some("modloader"), Some("loadorder")]; @@ -141,12 +151,12 @@ pub fn toggle_files( save_file: &Path, ) -> std::io::Result<()> { if *num_file == 1 { - save_path(save_file, Some("mod-files"), key, path_to_save[0])?; + save_path(save_file, INI_SECTIONS[3], key, path_to_save[0])?; } else { remove_array(save_file, key)?; save_paths(save_file, key, path_to_save)?; } - save_bool(save_file, Some("registered-mods"), key, state)?; + save_bool(save_file, INI_SECTIONS[2], key, state)?; Ok(()) } let num_rename_files = reg_mod.files.dll.len(); @@ -326,7 +336,7 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { return Ok(PathResult::None(PathBuf::new())); } }; - if let Ok(path) = IniProperty::::read(&config, Some("paths"), "game_dir", false) + if let Ok(path) = IniProperty::::read(&config, INI_SECTIONS[1], INI_KEYS[1], false) .and_then(|ini_property| { match does_dir_contain(&ini_property.value, Operation::All, &REQUIRED_GAME_FILES) { Ok(OperationResult { @@ -360,7 +370,12 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { .success { info!("Success: located \"game_dir\" on drive"); - save_path(file_name, Some("paths"), "game_dir", try_locate.as_path())?; + save_path( + file_name, + INI_SECTIONS[1], + INI_KEYS[1], + try_locate.as_path(), + )?; return Ok(PathResult::Full(try_locate)); } if try_locate.components().count() > 1 { diff --git a/src/main.rs b/src/main.rs index b815e91..a19d4f9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -119,8 +119,8 @@ fn main() -> Result<(), slint::PlatformError> { match IniProperty::::read( &get_cfg(current_ini).expect("ini file is verified"), - Some("app-settings"), - "dark_mode", + INI_SECTIONS[0], + INI_KEYS[0], false, ) { Ok(bool) => ui.global::().set_dark_mode(bool.value), @@ -128,7 +128,7 @@ fn main() -> Result<(), slint::PlatformError> { // io::Read error errors.push(err); ui.global::().set_dark_mode(true); - save_bool(current_ini, Some("app-settings"), "dark_mode", true) + save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], true) // io::Write error .unwrap_or_else(|err| errors.push(err)); } @@ -335,7 +335,7 @@ fn main() -> Result<(), slint::PlatformError> { let state = !files.iter().all(FileData::is_disabled); results.push(save_bool( current_ini, - Some("registered-mods"), + INI_SECTIONS[2], &format_key, state, )); @@ -343,7 +343,7 @@ fn main() -> Result<(), slint::PlatformError> { 0 => return, 1 => results.push(save_path( current_ini, - Some("mod-files"), + INI_SECTIONS[3], &format_key, files[0].as_path(), )), @@ -357,7 +357,7 @@ fn main() -> Result<(), slint::PlatformError> { // If something fails to save attempt to create a corrupt entry so // sync keys will take care of any invalid ini entries let _ = - remove_entry(current_ini, Some("registered-mods"), &format_key); + remove_entry(current_ini, INI_SECTIONS[2], &format_key); } let new_mod = RegMod::new(&format_key, state, files); @@ -369,7 +369,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&err.to_string()); let _ = remove_entry( current_ini, - Some("registered-mods"), + INI_SECTIONS[2], &new_mod.name, ); }; @@ -421,8 +421,8 @@ fn main() -> Result<(), slint::PlatformError> { }; match does_dir_contain(Path::new(&try_path), Operation::All, &REQUIRED_GAME_FILES) { Ok(OperationResult { success: true, files_found: _ }) => { - let result = save_path(current_ini, Some("paths"), "game_dir", &try_path); - if result.is_err() && save_path(current_ini, Some("paths"), "game_dir", &try_path).is_err() { + let result = save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path); + if result.is_err() && save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path).is_err() { let err = result.unwrap_err(); error!("Failed to save directory. {err}"); ui.display_msg(&err.to_string()); @@ -576,7 +576,7 @@ fn main() -> Result<(), slint::PlatformError> { if found_mod.files.len() == 1 { results.push(remove_entry( current_ini, - Some("mod-files"), + INI_SECTIONS[3], &found_mod.name, )); } else { @@ -587,7 +587,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&err.to_string()); let _ = remove_entry( current_ini, - Some("registered-mods"), + INI_SECTIONS[2], &format_key, ); } @@ -604,7 +604,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&err.to_string()); let _ = remove_entry( current_ini, - Some("registered-mods"), + INI_SECTIONS[2], &updated_mod.name, ); }; @@ -667,7 +667,7 @@ fn main() -> Result<(), slint::PlatformError> { } } // we can let sync keys take care of removing files from ini - remove_entry(current_ini, Some("registered-mods"), &found_mod.name) + remove_entry(current_ini, INI_SECTIONS[2], &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); let file_refs = found_mod.files.add_other_files_to_files(&found_files); let ui_handle = ui.as_weak(); @@ -706,7 +706,7 @@ fn main() -> Result<(), slint::PlatformError> { move |state| { let ui = ui_handle.unwrap(); let current_ini = get_ini_dir(); - save_bool(current_ini, Some("app-settings"), "dark_mode", state).unwrap_or_else( + save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], state).unwrap_or_else( |err| ui.display_msg(&format!("Failed to save theme preference\n\n{err}")), ); } @@ -1257,8 +1257,8 @@ async fn confirm_scan_mods( let dark_mode = ui.global::().get_dark_mode(); std::fs::remove_file(ini_file)?; new_cfg(ini_file)?; - save_bool(ini_file, Some("app-settings"), "dark_mode", dark_mode)?; - save_path(ini_file, Some("paths"), "game_dir", game_dir)?; + save_bool(ini_file, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; + save_path(ini_file, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; } scan_for_mods(game_dir, ini_file) } \ No newline at end of file diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 60feee8..e2f45e9 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -2,7 +2,6 @@ use ini::{Ini, Properties}; use log::{error, warn}; use std::{ collections::HashMap, - fmt::Debug, io::ErrorKind, path::{Path, PathBuf}, }; @@ -11,9 +10,9 @@ use crate::{ get_cfg, new_io_error, toggle_files, utils::ini::{ mod_loader::ModLoaderCfg, - writer::{remove_array, remove_entry, INI_SECTIONS}, + writer::{remove_array, remove_entry}, }, - FileData, LOADER_SECTIONS, OFF_STATE, + FileData, ARRAY_KEY, ARRAY_VALUE, INI_KEYS, INI_SECTIONS, LOADER_SECTIONS, OFF_STATE, }; pub trait Parsable: Sized { @@ -73,7 +72,7 @@ impl Parsable for PathBuf { ) -> std::io::Result { let parsed_value = PathBuf::from({ let value = ini.get_from(section, key); - if matches!(value, Some("array")) { + if matches!(value, Some(ARRAY_VALUE)) { return new_io_error!( ErrorKind::InvalidData, "Invalid type found. Expected: Path, Found: Vec" @@ -102,11 +101,11 @@ impl Parsable for Vec { .iter() .skip_while(|(k, _)| *k != key) .skip_while(|(k, _)| *k == key) - .take_while(|(k, _)| *k == "array[]") + .take_while(|(k, _)| *k == ARRAY_KEY) .map(|(_, v)| PathBuf::from(v)) .collect() } - if !matches!(ini.get_from(section, key), Some("array")) { + if !matches!(ini.get_from(section, key), Some(ARRAY_VALUE)) { return new_io_error!( ErrorKind::InvalidData, "Invalid type found. Expected: Vec, Found: Path" @@ -212,10 +211,9 @@ impl Setup for Ini { // MARK: FIXME // add functionality for matching Ini filename fn is_setup(&self) -> bool { - INI_SECTIONS.iter().all(|section| { - self.section(Some(section.trim_matches(|c| c == '[' || c == ']'))) - .is_some() - }) + INI_SECTIONS + .iter() + .all(|§ion| self.section(section).is_some()) } } @@ -251,12 +249,13 @@ impl IniProperty { true => { // This will have to be abstracted to the caller if we want the ability for the caller to specify the _path_prefix_ // right now _game_dir_ is the only valid prefix && "mod-files" is the only place _short_paths_ are stored - let game_dir = match section { - Some("mod-files") => Some( - IniProperty::::read(ini, Some("paths"), "game_dir", false)? + let game_dir = if section == INI_SECTIONS[3] { + Some( + IniProperty::::read(ini, INI_SECTIONS[1], INI_KEYS[1], false)? .value, - ), - _ => None, + ) + } else { + None }; T::parse_str(ini, section, game_dir, key, skip_validation) } @@ -456,24 +455,24 @@ impl RegMod { section .iter() .enumerate() - .filter(|(_, (k, _))| *k != "array[]") + .filter(|(_, (k, _))| *k != ARRAY_KEY) .map(|(i, (k, v))| { let paths = section .iter() .skip(i + 1) - .take_while(|(k, _)| *k == "array[]") + .take_while(|(k, _)| *k == ARRAY_KEY) .map(|(_, v)| v) .collect(); - (k, if v == "array" { paths } else { vec![v] }) + (k, if v == ARRAY_VALUE { paths } else { vec![v] }) }) .collect() } let mod_state_data = ini - .section(Some("registered-mods")) + .section(INI_SECTIONS[2]) .expect("Validated by Ini::is_setup on startup"); let dll_data = ini - .section(Some("mod-files")) + .section(INI_SECTIONS[3]) .expect("Validated by Ini::is_setup on startup"); let mut state_data = mod_state_data.iter().collect::>(); let mut file_data = collect_paths(dll_data); @@ -485,7 +484,7 @@ impl RegMod { for key in invalid_state { state_data.remove(key); - remove_entry(ini_path, Some("registered-mods"), key)?; + remove_entry(ini_path, INI_SECTIONS[2], key)?; warn!("\"{key}\" has no matching files"); } @@ -499,7 +498,7 @@ impl RegMod { if file_data.get(key).expect("key exists").len() > 1 { remove_array(ini_path, key)?; } else { - remove_entry(ini_path, Some("mod-files"), key)?; + remove_entry(ini_path, INI_SECTIONS[3], key)?; } file_data.remove(key); warn!("\"{key}\" has no matching state"); @@ -546,24 +545,24 @@ impl RegMod { fn collect_data_unchecked(ini: &Ini) -> Vec<(&str, &str, Vec<&str>)> { let mod_state_data = ini - .section(Some("registered-mods")) + .section(INI_SECTIONS[2]) .expect("Validated by Ini::is_setup on startup"); let dll_data = ini - .section(Some("mod-files")) + .section(INI_SECTIONS[3]) .expect("Validated by Ini::is_setup on startup"); dll_data .iter() .enumerate() - .filter(|(_, (k, _))| *k != "array[]") + .filter(|(_, (k, _))| *k != ARRAY_KEY) .map(|(i, (k, v))| { let paths = dll_data .iter() .skip(i + 1) - .take_while(|(k, _)| *k == "array[]") + .take_while(|(k, _)| *k == ARRAY_KEY) .map(|(_, v)| v) .collect::>(); let s = mod_state_data.get(k).expect("key exists"); - (k, s, if v == "array" { paths } else { vec![v] }) + (k, s, if v == ARRAY_VALUE { paths } else { vec![v] }) }) .collect() } @@ -585,7 +584,7 @@ impl RegMod { } else { let parsed_data = sync_keys(&ini, ini_path)?; let game_dir = - IniProperty::::read(&ini, Some("paths"), "game_dir", false)?.value; + IniProperty::::read(&ini, INI_SECTIONS[1], INI_KEYS[1], false)?.value; let load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))? .parse_section() @@ -597,15 +596,14 @@ impl RegMod { Ok(bool) => { if let Err(err) = f.file_refs().validate(Some(&game_dir)) { error!("Error: {err}"); - remove_entry(ini_path, Some("registered-mods"), k) - .expect("Key is valid"); + remove_entry(ini_path, INI_SECTIONS[2], k).expect("Key is valid"); } else { output.push(RegMod::from_split_files(k, *bool, f, l)) } } Err(err) => { error!("Error: {err}"); - remove_entry(ini_path, Some("registered-mods"), k).expect("Key is valid"); + remove_entry(ini_path, INI_SECTIONS[2], k).expect("Key is valid"); } } } @@ -683,79 +681,3 @@ impl ErrorClone for &std::io::Error { std::io::Error::new(self.kind(), self.to_string()) } } - -// ----------------------Optimized original implementation------------------------------- -// let mod_state_data = ini.section(Some("registered-mods")).unwrap(); -// mod_state_data -// .iter() -// .map(|(key, _)| RegMod { -// name: key.replace('_', " ").to_string(), -// state: IniProperty::::read(&ini, Some("registered-mods"), key) -// .unwrap() -// .value, -// files: if ini.get_from(Some("mod-files"), key).unwrap() == "array" { -// IniProperty::>::read(&ini, Some("mod-files"), key) -// .unwrap() -// .value -// } else { -// vec![ -// IniProperty::::read(&ini, Some("mod-files"), key) -// .unwrap() -// .value, -// ] -// }, -// }) -// .collect() -// ----------------------------------Multi-threaded attempt---------------------------------------- -// SLOW ASF -- Prolly because of how parser is setup -- try setting up parse_str to only take a str as input -// then pass each thread the strings it needs to parse -// let ini = Arc::new(get_cfg(path).unwrap()); -// let mod_state_data = ini.section(Some("registered-mods")).unwrap(); -// let (tx, rx) = mpsc::channel(); -// let mut found_data: Vec = Vec::with_capacity(mod_state_data.len()); -// for (key, _) in mod_state_data.iter() { -// let ini_clone = Arc::clone(&ini); -// let key_clone = String::from(key); -// let tx_clone = tx.clone(); -// thread::spawn(move || { -// tx_clone -// .send(RegMod { -// name: key_clone.replace('_', " "), -// state: IniProperty::::read( -// &ini_clone, -// Some("registered-mods"), -// &key_clone, -// ) -// .unwrap() -// .value, -// files: if ini_clone.get_from(Some("mod-files"), &key_clone).unwrap() -// == "array" -// { -// IniProperty::>::read( -// &ini_clone, -// Some("mod-files"), -// &key_clone, -// ) -// .unwrap() -// .value -// } else { -// vec![ -// IniProperty::::read( -// &ini_clone, -// Some("mod-files"), -// &key_clone, -// ) -// .unwrap() -// .value, -// ] -// }, -// }) -// .unwrap() -// }); -// } -// drop(tx); - -// for received in rx { -// found_data.push(received); -// } -// found_data diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index c6eb9d6..9845626 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -6,9 +6,9 @@ use std::{ path::Path, }; -use crate::{get_cfg, parent_or_err}; +use crate::{get_cfg, parent_or_err, INI_SECTIONS as SECTIONS}; -pub const INI_SECTIONS: [&str; 4] = [ +const INI_SECTIONS: [&str; 4] = [ "[app-settings]", "[paths]", "[registered-mods]", @@ -35,7 +35,7 @@ pub fn save_paths(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Resu .collect::>() .join("\r\narray[]="); config - .with_section(Some("mod-files")) + .with_section(SECTIONS[3]) .set(key, format!("array\r\narray[]={save_paths}")); config.write_to_file_opt(file_name, WRITE_OPTIONS) } diff --git a/src/utils/installer.rs b/src/utils/installer.rs index 0647faa..a25af45 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -11,7 +11,7 @@ use crate::{ parser::RegMod, writer::{save_bool, save_path, save_paths}, }, - FileData, + FileData, INI_SECTIONS, }; /// Returns the deepest occurance of a directory that contains at least 1 file @@ -561,15 +561,10 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result } } for mod_data in &file_sets { - save_bool( - ini_file, - Some("registered-mods"), - &mod_data.name, - mod_data.state, - )?; + save_bool(ini_file, INI_SECTIONS[2], &mod_data.name, mod_data.state)?; let file_refs = mod_data.files.file_refs(); if file_refs.len() == 1 { - save_path(ini_file, Some("mod-files"), &mod_data.name, file_refs[0])?; + save_path(ini_file, INI_SECTIONS[3], &mod_data.name, file_refs[0])?; } else { save_paths(ini_file, &mod_data.name, &file_refs)?; } diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 1f1ea8c..ac5e581 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -11,6 +11,7 @@ mod tests { parser::{IniProperty, RegMod, Setup}, writer::*, }, + INI_SECTIONS, }; #[test] @@ -22,7 +23,7 @@ mod tests { for (i, num) in test_nums.iter().enumerate() { save_value_ext( test_file, - Some("paths"), + INI_SECTIONS[1], &format!("test_num_{i}"), &num.to_string(), ) @@ -34,7 +35,7 @@ mod tests { for (i, num) in test_nums.iter().enumerate() { assert_eq!( *num, - IniProperty::::read(&config, Some("paths"), &format!("test_num_{i}"), false) + IniProperty::::read(&config, INI_SECTIONS[1], &format!("test_num_{i}"), false) .unwrap() .value ) @@ -53,7 +54,7 @@ mod tests { for (i, bool_str) in test_bools.iter().enumerate() { save_value_ext( test_file, - Some("paths"), + INI_SECTIONS[1], &format!("test_bool_{i}"), bool_str, ) @@ -65,9 +66,14 @@ mod tests { for (i, bool) in bool_results.iter().enumerate() { assert_eq!( *bool, - IniProperty::::read(&config, Some("paths"), &format!("test_bool_{i}"), false) - .unwrap() - .value + IniProperty::::read( + &config, + INI_SECTIONS[1], + &format!("test_bool_{i}"), + false + ) + .unwrap() + .value ) } @@ -83,16 +89,17 @@ mod tests { { new_cfg(test_file).unwrap(); - save_path(test_file, Some("paths"), "game_dir", test_path_1).unwrap(); - save_path(test_file, Some("paths"), "random_dir", test_path_2).unwrap(); + save_path(test_file, INI_SECTIONS[1], "game_dir", test_path_1).unwrap(); + save_path(test_file, INI_SECTIONS[1], "random_dir", test_path_2).unwrap(); } let config = get_cfg(test_file).unwrap(); - let parse_test_1 = IniProperty::::read(&config, Some("paths"), "game_dir", false) - .unwrap() - .value; + let parse_test_1 = + IniProperty::::read(&config, INI_SECTIONS[1], "game_dir", false) + .unwrap() + .value; let parse_test_2 = - IniProperty::::read(&config, Some("paths"), "random_dir", false) + IniProperty::::read(&config, INI_SECTIONS[1], "random_dir", false) .unwrap() .value; @@ -114,7 +121,7 @@ mod tests { let test_file = Path::new("temp\\test_type_check.ini"); new_cfg(test_file).unwrap(); - save_path(test_file, Some("paths"), "game_dir", test_path).unwrap(); + save_path(test_file, INI_SECTIONS[1], "game_dir", test_path).unwrap(); save_paths(test_file, "test_array", &test_array).unwrap(); let config = get_cfg(test_file).unwrap(); @@ -129,14 +136,14 @@ mod tests { ); let vec_result = - IniProperty::>::read(&config, Some("paths"), "game_dir", false); + IniProperty::>::read(&config, INI_SECTIONS[1], "game_dir", false); assert_eq!( vec_result.unwrap_err().to_string(), vec_pathbuf_err.to_string() ); let path_result = - IniProperty::::read(&config, Some("mod-files"), "test_array", false); + IniProperty::::read(&config, INI_SECTIONS[3], "test_array", false); assert_eq!( path_result.unwrap_err().to_string(), pathbuf_err.to_string() @@ -176,20 +183,20 @@ mod tests { Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); save_paths(test_file, mod_1_key, &mod_1).unwrap(); - save_bool(test_file, Some("registered-mods"), mod_1_key, mod_1_state).unwrap(); - save_path(test_file, Some("mod-files"), mod_2_key, &mod_2).unwrap(); - save_bool(test_file, Some("registered-mods"), mod_2_key, mod_2_state).unwrap(); + save_bool(test_file, INI_SECTIONS[2], mod_1_key, mod_1_state).unwrap(); + save_path(test_file, INI_SECTIONS[3], mod_2_key, &mod_2).unwrap(); + save_bool(test_file, INI_SECTIONS[2], mod_2_key, mod_2_state).unwrap(); save_paths(test_file, "no_matching_state_1", &invalid_format_1).unwrap(); save_path( test_file, - Some("mod-files"), + INI_SECTIONS[3], "no_matching_state_2", &invalid_format_2, ) .unwrap(); - save_bool(test_file, Some("registered-mods"), "no_matching_path", true).unwrap(); + save_bool(test_file, INI_SECTIONS[2], "no_matching_path", true).unwrap(); - save_path(test_file, Some("paths"), "game_dir", game_path).unwrap(); + save_path(test_file, INI_SECTIONS[1], "game_dir", game_path).unwrap(); } // -------------------------------------sync_keys runs from inside RegMod::collect()------------------------------------------------ diff --git a/ui/common.slint b/ui/common.slint index 2f98717..6541e6d 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -62,6 +62,8 @@ struct ButtonColors { hovered: color, } +// MARK: TODO +// make light mode look not like shit export global ColorPalette { out property page-background-color: SettingsLogic.dark-mode ? #1b1b1b : #60a0a4; out property alt-page-background-color: SettingsLogic.dark-mode ? #132b4e : #747e81; diff --git a/ui/tabs.slint b/ui/tabs.slint index e43e898..f59b876 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -164,6 +164,9 @@ export component ModEdit inherits Tab { model: MainLogic.current-mods[mod-index].dll-files; selected(file) => { modify-file(file, self.current-index) } } + + // MARK: TODO + // Create a focus scope to handle up and down arrow inputs if load-order.checked : SpinBox { width: 106px; enabled: load-order.checked; From e2039be1f3a1f9a76174ad49ba60da3e4e75bdc3 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 1 May 2024 20:55:45 -0500 Subject: [PATCH 40/62] test for verify_state --- .gitignore | 1 + src/lib.rs | 34 ++++++------- src/main.rs | 2 + src/utils/ini/parser.rs | 36 +++++++------ tests/test_ini_tools.rs | 109 ++++++++++++++++++++++++++++------------ 5 files changed, 111 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index 7b56e90..b00084a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /temp/ /.vscode/ /EML_gui_config.ini +rustfmt.toml # These are backup files generated by rustfmt **/*.rs.bk diff --git a/src/lib.rs b/src/lib.rs index b77f789..4fb0c16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,16 +74,14 @@ impl PathErrors { pub fn shorten_paths(paths: &[PathBuf], remove: &PathBuf) -> Result, PathErrors> { let mut results = PathErrors::new(paths.len()); - paths - .iter() - .for_each(|path| match path.strip_prefix(remove) { - Ok(file) => { - results.ok_paths_short.push(PathBuf::from(file)); - } - Err(_) => { - results.err_paths_long.push(PathBuf::from(path)); - } - }); + paths.iter().for_each(|path| match path.strip_prefix(remove) { + Ok(file) => { + results.ok_paths_short.push(PathBuf::from(file)); + } + Err(_) => { + results.err_paths_long.push(PathBuf::from(path)); + } + }); if results.err_paths_long.is_empty() { Ok(results.ok_paths_short) } else { @@ -91,6 +89,7 @@ pub fn shorten_paths(paths: &[PathBuf], remove: &PathBuf) -> Result } } +/// returns all the modified _partial_paths_ pub fn toggle_files( game_dir: &Path, new_state: bool, @@ -135,13 +134,10 @@ pub fn toggle_files( ); } - paths - .iter() - .zip(new_paths.iter()) - .try_for_each(|(path, new_path)| { - std::fs::rename(path, new_path)?; - Ok(()) - }) + paths.iter().zip(new_paths.iter()).try_for_each(|(path, new_path)| { + std::fs::rename(path, new_path)?; + Ok(()) + }) } fn update_cfg( num_file: &usize, @@ -235,9 +231,7 @@ pub fn does_dir_contain( }) } Operation::Any => { - let result = list - .iter() - .any(|&check_file| file_names.contains(check_file)); + let result = list.iter().any(|&check_file| file_names.contains(check_file)); Ok(OperationResult { success: result, files_found: if result { 1 } else { 0 }, diff --git a/src/main.rs b/src/main.rs index a19d4f9..958a79f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1255,6 +1255,8 @@ async fn confirm_scan_mods( return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to scan for mods"); }; let dark_mode = ui.global::().get_dark_mode(); + // MARK: TODO + // need to check if a deleted mod was in the disabled state and then toggle if so std::fs::remove_file(ini_file)?; new_cfg(ini_file)?; save_bool(ini_file, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index e2f45e9..08f6a4f 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -39,10 +39,7 @@ impl Parsable for bool { { "0" => Ok(false), "1" => Ok(true), - c => c - .to_lowercase() - .parse::() - .map_err(|err| err.into_io_error()), + c => c.to_lowercase().parse::().map_err(|err| err.into_io_error()), } } } @@ -112,8 +109,7 @@ impl Parsable for Vec { ); } let parsed_value = read_array( - ini.section(section) - .expect("Validated by IniProperty::is_valid"), + ini.section(section).expect("Validated by IniProperty::is_valid"), key, ); if skip_validation { @@ -175,9 +171,7 @@ fn validate_file(path: &Path) -> std::io::Result<()> { ErrorKind::InvalidInput, format!( "\"{}\" does not have an extention", - input_file - .split_at(if split != 0 { split + 1 } else { split }) - .1 + input_file.split_at(if split != 0 { split + 1 } else { split }).1 ) ); } @@ -211,9 +205,7 @@ impl Setup for Ini { // MARK: FIXME // add functionality for matching Ini filename fn is_setup(&self) -> bool { - INI_SECTIONS - .iter() - .all(|§ion| self.section(section).is_some()) + INI_SECTIONS.iter().all(|§ion| self.section(section).is_some()) } } @@ -404,7 +396,7 @@ impl RegMod { /// this function omits the population of the `order` field pub fn new(name: &str, state: bool, in_files: Vec) -> Self { RegMod { - name: String::from(name), + name: name.trim().replace(' ', "_"), state, files: SplitFiles::from(in_files), order: LoadOrder::default(), @@ -422,7 +414,7 @@ impl RegMod { let split_files = SplitFiles::from(in_files); let load_order = LoadOrder::from(&split_files.dll, parsed_order_val); RegMod { - name: String::from(name), + name: name.trim().replace(' ', "_"), state, files: split_files, order: load_order, @@ -441,6 +433,8 @@ impl RegMod { // MARK: FIXME? // when is the best time to verify parsed data? currently we verify data after shaping it // the code would most likely be cleaner if we verified it apon parsing before doing any shaping + + // should we have two collections? one for deserialization(full) one for just collect and verify pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { type CollectedMaps<'a> = (HashMap<&'a str, &'a str>, HashMap<&'a str, Vec<&'a str>>); type ModData<'a> = Vec<( @@ -585,10 +579,9 @@ impl RegMod { let parsed_data = sync_keys(&ini, ini_path)?; let game_dir = IniProperty::::read(&ini, INI_SECTIONS[1], INI_KEYS[1], false)?.value; - let load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) - .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))? + let load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1])? .parse_section() - .map_err(|err| std::io::Error::new(ErrorKind::InvalidData, err))?; + .map_err(|err| err.into_io_error())?; let parsed_data = combine_map_data(parsed_data, &load_order_parsed); let mut output = Vec::with_capacity(parsed_data.len()); for (k, s, f, l) in parsed_data { @@ -598,7 +591,12 @@ impl RegMod { error!("Error: {err}"); remove_entry(ini_path, INI_SECTIONS[2], k).expect("Key is valid"); } else { - output.push(RegMod::from_split_files(k, *bool, f, l)) + let reg_mod = RegMod::from_split_files(k, *bool, f, l); + // MARK: FIXME + // verify_state should be ran within collect, but this call is too late, we should handle verification earilier + // when sync keys hits an error we should give it a chance to correct by calling verify_state before it deletes an entry + reg_mod.verify_state(&game_dir, ini_path)?; + output.push(reg_mod) } } Err(err) => { @@ -619,7 +617,7 @@ impl RegMod { "wrong file state for \"{}\" chaning file extentions", self.name ); - toggle_files(game_dir, self.state, self, Some(ini_path)).map(|_| ())? + let _ = toggle_files(game_dir, self.state, self, Some(ini_path))?; } Ok(()) } diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index ac5e581..c40a7f9 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -11,9 +11,11 @@ mod tests { parser::{IniProperty, RegMod, Setup}, writer::*, }, - INI_SECTIONS, + INI_KEYS, INI_SECTIONS, OFF_STATE, }; + const GAME_DIR: &str = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; + #[test] fn does_u32_parse() { let test_nums: [u32; 3] = [2342652342, 2343523423, 69420]; @@ -82,20 +84,19 @@ mod tests { #[test] fn does_path_parse() { - let test_path_1 = - Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); + let test_path_1 = Path::new(GAME_DIR); let test_path_2 = Path::new("C:\\Windows\\System32"); let test_file = Path::new("temp\\test_path.ini"); { new_cfg(test_file).unwrap(); - save_path(test_file, INI_SECTIONS[1], "game_dir", test_path_1).unwrap(); + save_path(test_file, INI_SECTIONS[1], INI_KEYS[1], test_path_1).unwrap(); save_path(test_file, INI_SECTIONS[1], "random_dir", test_path_2).unwrap(); } let config = get_cfg(test_file).unwrap(); let parse_test_1 = - IniProperty::::read(&config, INI_SECTIONS[1], "game_dir", false) + IniProperty::::read(&config, INI_SECTIONS[1], INI_KEYS[1], false) .unwrap() .value; let parse_test_2 = @@ -112,8 +113,7 @@ mod tests { #[test] #[allow(unused_variables)] fn type_check() { - let test_path = - Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); + let test_path = Path::new(GAME_DIR); let test_array = [ Path::new("mods\\UnlockTheFps.dll"), Path::new("mods\\UnlockTheFps\\config.ini"), @@ -121,7 +121,7 @@ mod tests { let test_file = Path::new("temp\\test_type_check.ini"); new_cfg(test_file).unwrap(); - save_path(test_file, INI_SECTIONS[1], "game_dir", test_path).unwrap(); + save_path(test_file, INI_SECTIONS[1], INI_KEYS[1], test_path).unwrap(); save_paths(test_file, "test_array", &test_array).unwrap(); let config = get_cfg(test_file).unwrap(); @@ -136,7 +136,7 @@ mod tests { ); let vec_result = - IniProperty::>::read(&config, INI_SECTIONS[1], "game_dir", false); + IniProperty::>::read(&config, INI_SECTIONS[1], INI_KEYS[1], false); assert_eq!( vec_result.unwrap_err().to_string(), vec_pathbuf_err.to_string() @@ -155,16 +155,18 @@ mod tests { #[test] fn read_write_delete_from_ini() { let test_file = Path::new("temp\\test_collect_mod_data.ini"); - let mod_1_key = "Unlock The Fps "; - let mod_1_state = false; - let mod_2_key = "Skip The Intro"; - let mod_2_state = true; + let game_path = Path::new(GAME_DIR); + let mod_1 = vec![ - Path::new("mods\\UnlockTheFps.dll"), - Path::new("mods\\UnlockTheFps\\config.ini"), + PathBuf::from("mods\\UnlockTheFps.dll"), + PathBuf::from("mods\\UnlockTheFps\\config.ini"), ]; let mod_2 = PathBuf::from("mods\\SkipTheIntro.dll"); + // test_mod_2 state is set incorrectly + let test_mod_1 = RegMod::new("Unlock The Fps ", true, mod_1); + let mut test_mod_2 = RegMod::new(" Skip The Intro", false, vec![mod_2]); + { // Test if new_cfg will write all Sections to the file with .is_setup() new_cfg(test_file).unwrap(); @@ -179,13 +181,29 @@ mod tests { // We must save a working game_dir in the ini before we can parse entries in Section("mod-files") // -----------------------parser is set up to only parse valid entries--------------------------- // ----use case for entries in Section("mod-files") is to keep track of files within game_dir---- - let game_path = - Path::new("C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"); - save_paths(test_file, mod_1_key, &mod_1).unwrap(); - save_bool(test_file, INI_SECTIONS[2], mod_1_key, mod_1_state).unwrap(); - save_path(test_file, INI_SECTIONS[3], mod_2_key, &mod_2).unwrap(); - save_bool(test_file, INI_SECTIONS[2], mod_2_key, mod_2_state).unwrap(); + save_paths(test_file, &test_mod_1.name, &test_mod_1.files.file_refs()).unwrap(); + save_bool( + test_file, + INI_SECTIONS[2], + &test_mod_1.name, + test_mod_1.state, + ) + .unwrap(); + save_path( + test_file, + INI_SECTIONS[3], + &test_mod_2.name, + &test_mod_2.files.dll[0], + ) + .unwrap(); + save_bool( + test_file, + INI_SECTIONS[2], + &test_mod_2.name, + test_mod_2.state, + ) + .unwrap(); save_paths(test_file, "no_matching_state_1", &invalid_format_1).unwrap(); save_path( test_file, @@ -196,33 +214,60 @@ mod tests { .unwrap(); save_bool(test_file, INI_SECTIONS[2], "no_matching_path", true).unwrap(); - save_path(test_file, INI_SECTIONS[1], "game_dir", game_path).unwrap(); + save_path(test_file, INI_SECTIONS[1], INI_KEYS[1], game_path).unwrap(); } - // -------------------------------------sync_keys runs from inside RegMod::collect()------------------------------------------------ + // -------------------------------------sync_keys() runs from inside RegMod::collect()------------------------------------------------ // ----this deletes any keys that do not have a matching state eg. (key has state but no files, or key has files but no state)----- // this tests delete_entry && delete_array in this case we delete "no_matching_path", "no_matching_state_1", and "no_matching_state_2" let registered_mods = RegMod::collect(test_file, false).unwrap(); assert_eq!(registered_mods.len(), 2); + // verify_state() also runs from within RegMod::collect() lets see if changed the state of the mods .dll file + let mut disabled_state = game_path.join(format!( + "{}{}", + test_mod_2.files.dll[0].display(), + OFF_STATE + )); + assert!(matches!(&disabled_state.try_exists(), Ok(true))); + std::mem::swap(&mut test_mod_2.files.dll[0], &mut disabled_state); + + // lets set it correctly now + test_mod_2.state = true; + test_mod_2.verify_state(game_path, test_file).unwrap(); + std::mem::swap(&mut test_mod_2.files.dll[0], &mut disabled_state); + assert!(matches!( + game_path.join(&test_mod_2.files.dll[0]).try_exists(), + Ok(true) + )); + + test_mod_2.state = IniProperty::::read( + &get_cfg(test_file).unwrap(), + INI_SECTIONS[2], + &test_mod_2.name, + false, + ) + .unwrap() + .value; + // Tests name format is correct - let reg_mod_1: &RegMod = registered_mods + let reg_mod_1 = registered_mods .iter() - .find(|data| data.name == mod_1_key.trim()) + .find(|data| data.name == test_mod_1.name.trim()) .unwrap(); - let reg_mod_2: &RegMod = registered_mods + let reg_mod_2 = registered_mods .iter() - .find(|data| data.name == mod_2_key.trim()) + .find(|data| data.name == test_mod_2.name.trim()) .unwrap(); // Tests if PathBuf and Vec's from Section("mod-files") parse correctly | these are partial paths - assert_eq!(mod_1[0], reg_mod_1.files.dll[0]); - assert_eq!(mod_1[1], reg_mod_1.files.config[0]); - assert_eq!(mod_2, reg_mod_2.files.dll[0]); + assert_eq!(test_mod_1.files.dll[0], reg_mod_1.files.dll[0]); + assert_eq!(test_mod_1.files.config[0], reg_mod_1.files.config[0]); + assert_eq!(test_mod_2.files.dll[0], reg_mod_2.files.dll[0]); // Tests if bool was parsed correctly - assert_eq!(mod_1_state, reg_mod_1.state); - assert_eq!(mod_2_state, reg_mod_2.state); + assert_eq!(test_mod_1.state, reg_mod_1.state); + assert!(test_mod_2.state); remove_file(test_file).unwrap(); } From 110426b8258157969de9873c962a19957ecd1d9e Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Thu, 2 May 2024 20:42:19 -0500 Subject: [PATCH 41/62] Feat: now will create mod_loader_ini if not found updated does_dir_contain() now uses a Hashset internally for quick lookups. Depending on the input Operation enum the function will either return a bool for `Operation::All` and `Operation::Any` and return a tuple (count_usize, Hashset<&str>) of files found for `Operation::Count` this allows for easy lookups for determining if it is ok to create a new ".ini" file for _elden_mod_loader_ uncle bob would not approve of multiple return types but I think it's cool --- src/lib.rs | 97 ++++++++++++------------ src/main.rs | 59 +++++++++------ src/utils/ini/mod_loader.rs | 145 ++++++++++++------------------------ src/utils/ini/parser.rs | 26 +++++-- src/utils/ini/writer.rs | 40 ++++++---- src/utils/installer.rs | 30 +++----- tests/test_ini_tools.rs | 2 +- 7 files changed, 188 insertions(+), 211 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4fb0c16..7efcdf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,8 @@ pub const REQUIRED_GAME_FILES: [&str; 3] = [ ]; pub const OFF_STATE: &str = ".disabled"; + +pub const INI_NAME: &str = "EML_gui_config.ini"; pub const INI_SECTIONS: [Option<&str>; 4] = [ Some("app-settings"), Some("paths"), @@ -43,13 +45,18 @@ pub const INI_SECTIONS: [Option<&str>; 4] = [ Some("mod-files"), ]; pub const INI_KEYS: [&str; 2] = ["dark_mode", "game_dir"]; +pub const DEFAULT_INI_VALUES: [&str; 1] = ["true"]; pub const ARRAY_KEY: &str = "array[]"; pub const ARRAY_VALUE: &str = "array"; -pub const LOADER_FILES: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll"]; -pub const LOADER_FILES_DISABLED: [&str; 2] = ["mod_loader_config.ini", "dinput8.dll.disabled"]; +pub const LOADER_FILES: [&str; 3] = [ + "dinput8.dll.disabled", + "dinput8.dll", + "mod_loader_config.ini", +]; pub const LOADER_SECTIONS: [Option<&str>; 2] = [Some("modloader"), Some("loadorder")]; pub const LOADER_KEYS: [&str; 2] = ["load_delay", "show_terminal"]; +pub const DEFAULT_LOADER_VALUES: [&str; 2] = ["5000", "0"]; #[macro_export] macro_rules! new_io_error { @@ -193,52 +200,50 @@ pub fn get_cfg(input_file: &Path) -> std::io::Result { pub enum Operation { All, Any, + Count, } -#[derive(Default)] -pub struct OperationResult { - pub success: bool, - pub files_found: usize, +pub enum OperationResult<'a, T: ?Sized> { + Bool(bool), + Count((usize, HashSet<&'a T>)), } -pub fn does_dir_contain( +/// `Operation::All` and `Operation::Any` map to `OperationResult::bool(_result_)` +/// `Operation::Count` maps to `OperationResult::Count((_num_found_, _HashSet<_&input_list_>))` +/// when matching you will always have to `_ => unreachable()` for the return type you will never get +pub fn does_dir_contain<'a, T>( path: &Path, operation: Operation, - list: &[&str], -) -> std::io::Result { + list: &'a [&T], +) -> std::io::Result> +where + T: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash + ?Sized, + for<'b> &'b str: std::borrow::Borrow, +{ let entries = std::fs::read_dir(path)?; let file_names = entries - // MARK: FIXME - // would be nice if we could leave as a OsString here - // change count to be the actual file found - // can we make a cleaner interface? - // not force to match against all file situations? use enum to return different data types? - // use bool for called to decide if they want to match against extra data? - .map(|entry| Ok(entry?.file_name().to_string_lossy().to_string())) - .collect::>>()?; + .filter_map(|entry| Some(entry.ok()?.file_name())) + .collect::>(); + let str_names = file_names.iter().filter_map(|f| f.to_str()).collect::>(); match operation { - Operation::All => { - let mut count = 0_usize; - list.iter().for_each(|&check_file| { - if file_names.contains(check_file) { - count += 1 - } - }); - Ok(OperationResult { - success: count == list.len(), - files_found: count, - }) - } - Operation::Any => { - let result = list.iter().any(|&check_file| file_names.contains(check_file)); - Ok(OperationResult { - success: result, - files_found: if result { 1 } else { 0 }, - }) + Operation::All => Ok(OperationResult::Bool( + list.iter().all(|&check_file| str_names.contains(check_file)), + )), + Operation::Any => Ok(OperationResult::Bool( + list.iter().any(|&check_file| str_names.contains(check_file)), + )), + Operation::Count => { + let collection = list + .iter() + .filter(|&check_file| str_names.contains(check_file)) + .copied() + .collect::>(); + Ok(OperationResult::Count((collection.len(), collection))) } } } + pub struct FileData<'a> { pub name: &'a str, pub extension: &'a str, @@ -332,26 +337,22 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { }; if let Ok(path) = IniProperty::::read(&config, INI_SECTIONS[1], INI_KEYS[1], false) .and_then(|ini_property| { + // right now all we do is log this error if we want to bail on the fn we can use the ? operator here match does_dir_contain(&ini_property.value, Operation::All, &REQUIRED_GAME_FILES) { - Ok(OperationResult { - success: true, - files_found: _, - }) => Ok(ini_property.value), - Ok(OperationResult { - success: false, - files_found: _, - }) => { + Ok(OperationResult::Bool(true)) => Ok(ini_property.value), + Ok(OperationResult::Bool(false)) => { let err = format!( "Required Game files not found in:\n\"{}\"", ini_property.value.display() ); - error!("{err}",); + error!("{err}"); new_io_error!(ErrorKind::NotFound, err) } Err(err) => { error!("Error: {err}"); Err(err) } + _ => unreachable!(), } }) { @@ -359,10 +360,10 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { return Ok(PathResult::Full(path)); } let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); - if does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES) - .unwrap_or_default() - .success - { + if matches!( + does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES), + Ok(OperationResult::Bool(true)) + ) { info!("Success: located \"game_dir\" on drive"); save_path( file_name, diff --git a/src/main.rs b/src/main.rs index 958a79f..004a2d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -#![windows_subsystem = "windows"] +// #![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ @@ -29,8 +29,6 @@ use tokio::sync::{ slint::include_modules!(); -const CONFIG_NAME: &str = "EML_gui_config.ini"; -const DEFAULT_VALUES: [&str; 2] = ["5000", "0"]; static GLOBAL_NUM_KEY: AtomicU32 = AtomicU32::new(0); static RESTRICTED_FILES: OnceLock<[&'static OsStr; 6]> = OnceLock::new(); static RECEIVER: OnceLock>> = OnceLock::new(); @@ -58,7 +56,7 @@ fn main() -> Result<(), slint::PlatformError> { let mut errors= Vec::new(); let ini_valid = match get_cfg(current_ini) { Ok(ini) => { - if ini.is_setup() { + if ini.is_setup(&INI_SECTIONS) { info!("Config file found at \"{}\"", current_ini.display()); first_startup = false; true @@ -70,7 +68,9 @@ fn main() -> Result<(), slint::PlatformError> { Err(err) => { // io::Open error or | parse error with type ErrorKind::InvalidData error!("Error: {err}"); - errors.push(err); + if err.kind() == ErrorKind::InvalidData { + errors.push(err); + } first_startup = true; false } @@ -151,7 +151,10 @@ fn main() -> Result<(), slint::PlatformError> { mod_loader = ModLoader::default(); } else { let game_dir = game_dir.expect("game dir verified"); - mod_loader = ModLoader::properties(&game_dir); + mod_loader = ModLoader::properties(&game_dir).unwrap_or_else(|err| { + errors.push(err); + ModLoader::default() + }); deserialize_current_mods( &match reg_mods { Some(Ok(mod_data)) => mod_data, @@ -175,13 +178,13 @@ fn main() -> Result<(), slint::PlatformError> { )); error!("{err}"); errors.push(err); - save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_VALUES[0]) + save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_LOADER_VALUES[0]) .unwrap_or_else(|err| { // io::write error error!("{err}"); errors.push(err); }); - DEFAULT_VALUES[0].parse().unwrap() + DEFAULT_LOADER_VALUES[0].parse().unwrap() }); let show_terminal = loader_cfg.get_show_terminal().unwrap_or_else(|_| { // parse error ErrorKind::InvalidData @@ -191,7 +194,7 @@ fn main() -> Result<(), slint::PlatformError> { )); error!("{err}"); errors.push(err); - save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_VALUES[1]) + save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_LOADER_VALUES[1]) .unwrap_or_else(|err| { // io::write error error!("{err}"); @@ -411,16 +414,17 @@ fn main() -> Result<(), slint::PlatformError> { info!("User Selected Path: \"{}\"", path.display()); let try_path: PathBuf = match does_dir_contain(&path, Operation::All, &["Game"]) { - Ok(OperationResult { success: true, files_found: _ }) => PathBuf::from(&format!("{}\\Game", path.display())), - Ok(OperationResult { success: false, files_found: _ }) => path, + Ok(OperationResult::Bool(true)) => PathBuf::from(&format!("{}\\Game", path.display())), + Ok(OperationResult::Bool(false)) => path, Err(err) => { error!("{err}"); ui.display_msg(&err.to_string()); return; } + _ => unreachable!(), }; match does_dir_contain(Path::new(&try_path), Operation::All, &REQUIRED_GAME_FILES) { - Ok(OperationResult { success: true, files_found: _ }) => { + Ok(OperationResult::Bool(true)) => { let result = save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path); if result.is_err() && save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path).is_err() { let err = result.unwrap_err(); @@ -429,7 +433,7 @@ fn main() -> Result<(), slint::PlatformError> { return; }; info!("Success: Files found, saved diretory"); - let mod_loader = ModLoader::properties(&try_path); + let mod_loader = ModLoader::properties(&try_path).unwrap_or_default(); ui.global::() .set_game_path(try_path.to_string_lossy().to_string().into()); let _ = get_or_update_game_dir(Some(try_path)); @@ -443,7 +447,7 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") } } - Ok(OperationResult { success: false, files_found: _ }) => { + Ok(OperationResult::Bool(false)) => { let err = format!("Required Game files not found in:\n\"{}\"", try_path.display()); error!("{err}"); ui.display_msg(&err); @@ -455,6 +459,7 @@ fn main() -> Result<(), slint::PlatformError> { } ui.display_msg(&err.to_string()) } + _ => unreachable!(), } }).unwrap(); } @@ -745,7 +750,13 @@ fn main() -> Result<(), slint::PlatformError> { move |state| -> bool { let ui = ui_handle.unwrap(); let value = if state { "1" } else { "0" }; - let ext_ini = get_or_update_game_dir(None).join(LOADER_FILES[0]); + let ext_ini = match ModLoader::properties(&get_or_update_game_dir(None)) { + Ok(ini) => ini.own_path(), + Err(err) => { + ui.display_msg(&err.to_string()); + return !state + } + }; let mut result = state; save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( |err| { @@ -760,7 +771,13 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |time| { let ui = ui_handle.unwrap(); - let ext_ini = get_or_update_game_dir(None).join(LOADER_FILES[0]); + let ext_ini = match ModLoader::properties(&get_or_update_game_dir(None)) { + Ok(ini) => ini.own_path(), + Err(err) => { + ui.display_msg(&err.to_string()); + return + } + }; ui.global::().invoke_force_app_focus(); if let Err(err) = save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { ui.display_msg(&format!("Failed to set load delay\n\n{err}")); @@ -780,7 +797,7 @@ fn main() -> Result<(), slint::PlatformError> { let files = if state { vec![PathBuf::from(LOADER_FILES[1])] } else { - vec![PathBuf::from(LOADER_FILES_DISABLED[1])] + vec![PathBuf::from(LOADER_FILES[0])] }; let main_dll = RegMod::new("main", !state, files); match toggle_files(&game_dir, !state, &main_dll, None) { @@ -833,7 +850,7 @@ fn main() -> Result<(), slint::PlatformError> { match confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, true).await { Ok(len) => { ui.global::().set_current_subpage(0); - let mod_loader = ModLoader::properties(&game_dir); + let mod_loader = ModLoader::properties(&game_dir).unwrap_or_default(); deserialize_current_mods( &RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); @@ -1008,7 +1025,7 @@ fn get_ini_dir() -> &'static PathBuf { static CONFIG_PATH: OnceLock = OnceLock::new(); CONFIG_PATH.get_or_init(|| { let exe_dir = std::env::current_dir().expect("Failed to get current dir"); - exe_dir.join(CONFIG_NAME) + exe_dir.join(INI_NAME) }) } @@ -1027,15 +1044,13 @@ fn get_or_update_game_dir(update: Option) -> tokio::sync::RwLockReadGua } fn populate_restricted_files() -> [&'static OsStr; 6] { - let mut restricted_files: [&OsStr; 6] = [&OsStr::new(""); 6]; + let mut restricted_files: [&OsStr; 6] = [OsStr::new(""); 6]; for (i, file) in LOADER_FILES.iter().map(OsStr::new).enumerate() { restricted_files[i] = file; } for (i, file) in REQUIRED_GAME_FILES.iter().map(OsStr::new).enumerate() { restricted_files[i + LOADER_FILES.len()] = file; } - restricted_files[LOADER_FILES.len() + REQUIRED_GAME_FILES.len()] = - OsStr::new(LOADER_FILES_DISABLED[1]); restricted_files } diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index fd9c042..c865e8c 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -1,5 +1,4 @@ use ini::Ini; -use log::{error, info, trace}; use std::{ collections::HashMap, io::ErrorKind, @@ -7,13 +6,12 @@ use std::{ }; use crate::{ - new_io_error, + does_dir_contain, get_cfg, new_io_error, utils::ini::{ - parser::{IniProperty, ModError, RegMod}, - writer::EXT_OPTIONS, + parser::{IniProperty, ModError, RegMod, Setup}, + writer::{new_cfg, EXT_OPTIONS}, }, - OperationResult, LOADER_KEYS, LOADER_SECTIONS, - {does_dir_contain, get_cfg, Operation, LOADER_FILES, LOADER_FILES_DISABLED}, + Operation, OperationResult, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, }; #[derive(Default)] @@ -24,55 +22,40 @@ pub struct ModLoader { } impl ModLoader { - pub fn properties(game_dir: &Path) -> ModLoader { - match does_dir_contain(game_dir, Operation::All, &LOADER_FILES) { - // MARK: IMPL FEAT? - // add branch for if ini not found then create ini with default values - Ok(OperationResult { - success: true, - files_found: _, - }) => { - info!("Found mod loader files"); - ModLoader { - installed: true, - disabled: false, - path: game_dir.join(LOADER_FILES[0]), - } - } - Ok(OperationResult { - success: false, - files_found: _, - }) => { - trace!("Checking if mod loader is disabled"); - match does_dir_contain(game_dir, Operation::All, &LOADER_FILES_DISABLED) { - Ok(OperationResult { - success: true, - files_found: _, - }) => { - info!("Found mod loader files in the disabled state"); - ModLoader { - installed: true, - disabled: true, - path: game_dir.join(LOADER_FILES[0]), - } + pub fn properties(game_dir: &Path) -> std::io::Result { + let cfg_dir = game_dir.join(LOADER_FILES[2]); + match does_dir_contain(game_dir, Operation::Count, &LOADER_FILES) { + Ok(OperationResult::Count((_, files))) => { + if files.contains(LOADER_FILES[1]) || !files.contains(LOADER_FILES[0]) { + if !files.contains(LOADER_FILES[2]) { + new_cfg(&cfg_dir)?; } - Ok(OperationResult { - success: false, - files_found: _, - }) => { - error!("Mod Loader Files not found in selected path"); - ModLoader::default() - } - Err(err) => { - error!("{err}"); - ModLoader::default() + Ok(ModLoader { + installed: true, + disabled: false, + path: cfg_dir, + }) + } else if files.contains(LOADER_FILES[0]) || !files.contains(LOADER_FILES[1]) { + if !files.contains(LOADER_FILES[2]) { + new_cfg(&cfg_dir)?; } + Ok(ModLoader { + installed: true, + disabled: true, + path: cfg_dir, + }) + } else { + return new_io_error!( + ErrorKind::InvalidData, + format!( + "Elden Mod Loader is not installed at: {}", + game_dir.display() + ) + ); } } - Err(err) => { - error!("{err}"); - ModLoader::default() - } + Err(err) => Err(err), + _ => unreachable!(), } } @@ -90,6 +73,11 @@ impl ModLoader { pub fn path(&self) -> &Path { &self.path } + + #[inline] + pub fn own_path(self) -> PathBuf { + self.path + } } #[derive(Default)] @@ -104,33 +92,13 @@ impl ModLoaderCfg { if section.is_none() { return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); } - let cfg_dir = match does_dir_contain(game_dir, Operation::All, &[LOADER_FILES[0]]) { - Ok(OperationResult { - success: true, - files_found: _, - }) => game_dir.join(LOADER_FILES[0]), - Ok(OperationResult { - success: false, - files_found: _, - }) => { - return new_io_error!( - ErrorKind::NotFound, - "\"mod_loader_config.ini\" does not exist in the current game_dir" - ); - } - Err(err) => return Err(err), - }; - let mut cfg = match get_cfg(&cfg_dir) { - Ok(ini) => ini, - Err(err) => { - return new_io_error!( - ErrorKind::NotFound, - format!("Could not read \"mod_loader_config.ini\"\n{err}") - ) - } - }; + let cfg_dir = ModLoader::properties(game_dir)?.path; + let mut cfg = get_cfg(&cfg_dir)?; + if !cfg.is_setup(&LOADER_SECTIONS) { + new_cfg(&cfg_dir)?; + } if cfg.section(section).is_none() { - ModLoaderCfg::init_section(&mut cfg, section)? + cfg.init_section(section)? } Ok(ModLoaderCfg { cfg, @@ -139,28 +107,6 @@ impl ModLoaderCfg { }) } - pub fn update_section(&mut self, section: Option<&str>) -> std::io::Result<()> { - if self.cfg.section(section).is_none() { - ModLoaderCfg::init_section(&mut self.cfg, section)? - }; - Ok(()) - } - - fn init_section(cfg: &mut ini::Ini, section: Option<&str>) -> std::io::Result<()> { - trace!( - "Section: \"{}\" not found creating new", - section.expect("Passed in section not valid") - ); - cfg.with_section(section).set("setter_temp_val", "0"); - if cfg.delete_from(section, "setter_temp_val").is_none() { - return new_io_error!( - ErrorKind::BrokenPipe, - format!("Failed to create a new section: \"{}\"", section.unwrap()) - ); - }; - Ok(()) - } - pub fn get_load_delay(&self) -> std::io::Result { match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], false) { Ok(delay_time) => Ok(delay_time.value), @@ -262,6 +208,7 @@ pub trait Countable { } impl<'a> Countable for &'a [RegMod] { + #[inline] fn order_count(&self) -> usize { self.iter().filter(|m| m.order.set).count() } diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 08f6a4f..ab302f0 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -1,5 +1,5 @@ use ini::{Ini, Properties}; -use log::{error, warn}; +use log::{error, trace, warn}; use std::{ collections::HashMap, io::ErrorKind, @@ -198,14 +198,28 @@ fn validate_existance(path: &Path) -> std::io::Result<()> { } pub trait Setup { - fn is_setup(&self) -> bool; + fn is_setup(&self, sections: &[Option<&str>]) -> bool; + fn init_section(&mut self, section: Option<&str>) -> std::io::Result<()>; } impl Setup for Ini { - // MARK: FIXME - // add functionality for matching Ini filename - fn is_setup(&self) -> bool { - INI_SECTIONS.iter().all(|§ion| self.section(section).is_some()) + fn is_setup(&self, sections: &[Option<&str>]) -> bool { + sections.iter().all(|§ion| self.section(section).is_some()) + } + + fn init_section(&mut self, section: Option<&str>) -> std::io::Result<()> { + trace!( + "Section: \"{}\" not found creating new", + section.expect("Passed in section not valid") + ); + self.with_section(section).set("setter_temp_val", "0"); + if self.delete_from(section, "setter_temp_val").is_none() { + return new_io_error!( + ErrorKind::BrokenPipe, + format!("Failed to create a new section: \"{}\"", section.unwrap()) + ); + }; + Ok(()) } } diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 9845626..23fcae7 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -6,14 +6,10 @@ use std::{ path::Path, }; -use crate::{get_cfg, parent_or_err, INI_SECTIONS as SECTIONS}; - -const INI_SECTIONS: [&str; 4] = [ - "[app-settings]", - "[paths]", - "[registered-mods]", - "[mod-files]", -]; +use crate::{ + file_name_or_err, get_cfg, parent_or_err, DEFAULT_INI_VALUES, DEFAULT_LOADER_VALUES, INI_KEYS, + INI_SECTIONS, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, +}; const WRITE_OPTIONS: WriteOption = WriteOption { escape_policy: EscapePolicy::Nothing, @@ -35,7 +31,7 @@ pub fn save_paths(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Resu .collect::>() .join("\r\narray[]="); config - .with_section(SECTIONS[3]) + .with_section(INI_SECTIONS[3]) .set(key, format!("array\r\narray[]={save_paths}")); config.write_to_file_opt(file_name, WRITE_OPTIONS) } @@ -76,14 +72,29 @@ pub fn save_value_ext( } pub fn new_cfg(path: &Path) -> std::io::Result<()> { + let file_name = file_name_or_err(path)?; let parent = parent_or_err(path)?; + fs::create_dir_all(parent)?; let mut new_ini = File::create(path)?; - for section in INI_SECTIONS { - writeln!(new_ini, "{section}")?; + if file_name == LOADER_FILES[2] { + for (i, section) in LOADER_SECTIONS.iter().enumerate() { + writeln!(new_ini, "[{}]", section.unwrap())?; + if i == 0 { + for (j, _) in LOADER_KEYS.iter().enumerate() { + writeln!(new_ini, "{} = {}", LOADER_KEYS[j], DEFAULT_LOADER_VALUES[j])? + } + } + } + } else { + for (i, section) in INI_SECTIONS.iter().enumerate() { + writeln!(new_ini, "[{}]", section.unwrap())?; + if i == 0 { + writeln!(new_ini, "{}={}", INI_KEYS[i], DEFAULT_INI_VALUES[i])? + } + } } - Ok(()) } @@ -105,10 +116,7 @@ pub fn remove_array(file_name: &Path, key: &str) -> std::io::Result<()> { !skip_next_line }; - let lines = content - .lines() - .filter(|&line| filter_lines(line)) - .collect::>(); + let lines = content.lines().filter(|&line| filter_lines(line)).collect::>(); write(file_name, lines.join("\r\n")) } diff --git a/src/utils/installer.rs b/src/utils/installer.rs index a25af45..532aaa9 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -398,7 +398,10 @@ impl InstallData { let game_dir = self_clone.install_dir.parent().expect("has parent"); if valid_dir.strip_prefix(game_dir).is_ok() { return new_io_error!(ErrorKind::InvalidInput, "Files are already installed"); - } else if does_dir_contain(&valid_dir, crate::Operation::All, &["mods"])?.success { + } else if matches!( + does_dir_contain(&valid_dir, crate::Operation::All, &["mods"])?, + crate::OperationResult::Bool(true) + ) { return new_io_error!(ErrorKind::InvalidData, "Invalid file structure"); } @@ -425,7 +428,7 @@ impl InstallData { trace!("New directory selected contains unique files, entire folder will be moved"); match items_in_directory(&valid_dir, FileType::Dir)? == 0 { true => self_clone.parent_dir = parent_or_err(&valid_dir)?.to_path_buf(), - false => self_clone.parent_dir = valid_dir.clone(), + false => self_clone.parent_dir.clone_from(&valid_dir), } } @@ -455,10 +458,7 @@ impl InstallData { pub fn remove_mod_files(game_dir: &Path, files: Vec<&Path>) -> std::io::Result<()> { let remove_files = files.iter().map(|f| game_dir.join(f)).collect::>(); - if remove_files - .iter() - .any(|file| !matches!(file.try_exists(), Ok(true))) - { + if remove_files.iter().any(|file| !matches!(file.try_exists(), Ok(true))) { return new_io_error!( ErrorKind::InvalidInput, "Could not confirm existance of all files to remove" @@ -497,6 +497,8 @@ pub fn remove_mod_files(game_dir: &Path, files: Vec<&Path>) -> std::io::Result<( Ok(()) } })?; + // MARK: TODO + // if mod has load order remove load order Ok(()) } @@ -531,10 +533,7 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result if file_data.extension != ".dll" { continue; }; - if let Some(dir) = dirs - .iter() - .find(|d| d.file_name().expect("is dir") == file_data.name) - { + if let Some(dir) = dirs.iter().find(|d| d.file_name().expect("is dir") == file_data.name) { let mut data = InstallData::new(file_data.name, vec![file.clone()], game_dir)?; data.import_files_from_dir(dir, &DisplayItems::None)?; file_sets.push(RegMod::new( @@ -542,21 +541,14 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result file_data.enabled, data.from_paths .into_iter() - .map(|p| { - p.strip_prefix(game_dir) - .expect("file found here") - .to_path_buf() - }) + .map(|p| p.strip_prefix(game_dir).expect("file found here").to_path_buf()) .collect::>(), )); } else { file_sets.push(RegMod::new( file_data.name, file_data.enabled, - vec![file - .strip_prefix(game_dir) - .expect("file found here") - .to_path_buf()], + vec![file.strip_prefix(game_dir).expect("file found here").to_path_buf()], )); } } diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index c40a7f9..b22d6a8 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -170,7 +170,7 @@ mod tests { { // Test if new_cfg will write all Sections to the file with .is_setup() new_cfg(test_file).unwrap(); - assert!(get_cfg(test_file).unwrap().is_setup()); + assert!(get_cfg(test_file).unwrap().is_setup(&INI_SECTIONS)); let invalid_format_1 = vec![ Path::new("mods\\UnlockTheFps.dll"), From 7db401c6b4ab12665f0805247a4b5dac54a2803c Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Thu, 2 May 2024 22:08:04 -0500 Subject: [PATCH 42/62] Feat: Load order removal on Mod deletion apon the removal of a mods files from the game dir the app now removes the load_order if it is set --- src/main.rs | 21 +++++++++++---------- src/utils/ini/parser.rs | 8 ++++++++ src/utils/installer.rs | 27 +++++++++++++++++++++------ 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index 004a2d7..945f83d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -649,7 +649,7 @@ fn main() -> Result<(), slint::PlatformError> { if receive_msg().await != Message::Confirm { return } - let reg_mods = match RegMod::collect(current_ini, false) { + let mut reg_mods = match RegMod::collect(current_ini, false) { Ok(reg_mods) => reg_mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -659,12 +659,14 @@ fn main() -> Result<(), slint::PlatformError> { }; let game_dir = get_or_update_game_dir(None); if let Some(found_mod) = - reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) + reg_mods.iter_mut().find(|reg_mod| format_key == reg_mod.name) { - let mut found_files = found_mod.files.dll.clone(); - if found_files.iter().any(FileData::is_disabled) { + if found_mod.files.dll.iter().any(FileData::is_disabled) { match toggle_files(&game_dir, true, found_mod, Some(current_ini)) { - Ok(files) => found_files = files, + Ok(files) => { + found_mod.files.dll = files; + found_mod.state = true; + }, Err(err) => { ui.display_msg(&format!("Failed to set mod to enabled state on removal\naborted before removal\n\n{err}")); return; @@ -674,9 +676,8 @@ fn main() -> Result<(), slint::PlatformError> { // we can let sync keys take care of removing files from ini remove_entry(current_ini, INI_SECTIONS[2], &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); - let file_refs = found_mod.files.add_other_files_to_files(&found_files); let ui_handle = ui.as_weak(); - match confirm_remove_mod(ui_handle, &game_dir, file_refs).await { + match confirm_remove_mod(ui_handle, &game_dir, found_mod).await { Ok(_) => ui.display_msg(&format!("Successfully removed all files associated with the previously registered mod \"{key}\"")), Err(err) => { match err.kind() { @@ -1237,9 +1238,9 @@ async fn confirm_install( async fn confirm_remove_mod( ui_weak: slint::Weak, - game_dir: &Path, files: Vec<&Path>) -> std::io::Result<()> { + game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { let ui = ui_weak.unwrap(); - let install_dir = match files.iter().min_by_key(|file| file.ancestors().count()) { + let install_dir = match reg_mod.files.file_refs().iter().min_by_key(|file| file.ancestors().count()) { Some(path) => game_dir.join(parent_or_err(path)?), None => PathBuf::from("Error: Failed to display a parent_dir"), }; @@ -1251,7 +1252,7 @@ async fn confirm_remove_mod( if receive_msg().await != Message::Confirm { return new_io_error!(ErrorKind::ConnectionAborted, format!("Mod files are still installed at \"{}\"", install_dir.display())); }; - remove_mod_files(game_dir, files) + remove_mod_files(game_dir, reg_mod) } async fn confirm_scan_mods( diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index ab302f0..6cb1f17 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -635,6 +635,14 @@ impl RegMod { } Ok(()) } + + pub fn update_dlls(&mut self, new: Vec) { + self.files.dll = new + } + + pub fn update_state(&mut self, new: bool) { + self.state = new + } } pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { diff --git a/src/utils/installer.rs b/src/utils/installer.rs index 532aaa9..a2f5d5b 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -11,9 +11,11 @@ use crate::{ parser::RegMod, writer::{save_bool, save_path, save_paths}, }, - FileData, INI_SECTIONS, + FileData, INI_SECTIONS, LOADER_SECTIONS, }; +use super::ini::{mod_loader::ModLoader, writer::remove_entry}; + /// Returns the deepest occurance of a directory that contains at least 1 file /// Use parent_or_err for a direct binding to what is one level up fn get_parent_dir(input: &Path) -> std::io::Result { @@ -455,8 +457,13 @@ impl InstallData { } } -pub fn remove_mod_files(game_dir: &Path, files: Vec<&Path>) -> std::io::Result<()> { - let remove_files = files.iter().map(|f| game_dir.join(f)).collect::>(); +pub fn remove_mod_files(game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { + let remove_files = reg_mod + .files + .file_refs() + .iter() + .map(|f| game_dir.join(f)) + .collect::>(); if remove_files.iter().any(|file| !matches!(file.try_exists(), Ok(true))) { return new_io_error!( @@ -497,9 +504,17 @@ pub fn remove_mod_files(game_dir: &Path, files: Vec<&Path>) -> std::io::Result<( Ok(()) } })?; - // MARK: TODO - // if mod has load order remove load order - + if reg_mod.order.set { + let file_name = file_name_or_err(®_mod.files.dll[reg_mod.order.i])?; + remove_entry( + ModLoader::properties(game_dir)?.path(), + LOADER_SECTIONS[1], + file_name.to_str().ok_or(std::io::Error::new( + ErrorKind::InvalidData, + format!("{file_name:?} is not valid UTF-8"), + ))?, + )?; + } Ok(()) } From a809af1d03689a2f7cc1ff98623ef0b93335378b Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Fri, 3 May 2024 01:22:48 -0500 Subject: [PATCH 43/62] parse_order_data improvments parse_order_data now checks recovers from usize.parse() errors also sets the orders correctly if they are not stored improperly this makes it so the ui always shows the mod with position 1 as order 1 no more clone in update_order_entries() now use mem::swap and mem::take to simplify the code and reduce on a not necessary clone --- Cargo.lock | 35 ++++++++------ src/main.rs | 16 ++----- src/utils/ini/mod_loader.rs | 93 +++++++++++++++++++++++-------------- src/utils/ini/parser.rs | 18 +++---- src/utils/installer.rs | 5 +- 5 files changed, 89 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49db3ab..3e5a093 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -185,47 +185,48 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -887,9 +888,9 @@ dependencies = [ [[package]] name = "clru" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" +checksum = "cbd0f76e066e64fdc5631e3bb46381254deab9ef1158292f27c8c57e3bf3fe59" [[package]] name = "cocoa" @@ -945,9 +946,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" @@ -2436,6 +2437,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" diff --git a/src/main.rs b/src/main.rs index 945f83d..eee5dbf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,7 @@ use elden_mod_loader_gui::{ utils::{ ini::{ - mod_loader::{ModLoader, ModLoaderCfg, update_order_entries, Countable}, + mod_loader::{ModLoader, ModLoaderCfg, Countable}, parser::{file_registered, IniProperty, RegMod, Setup, ErrorClone}, writer::*, }, @@ -895,12 +895,7 @@ fn main() -> Result<(), slint::PlatformError> { None } }; - update_order_entries(stable_k, load_orders).unwrap_or_else(|err| { - ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); - result = error; - std::mem::swap(load_orders, &mut ini::Properties::new()); - }); - load_order.write_to_file().unwrap_or_else(|err| { + load_order.update_order_entries(stable_k).unwrap_or_else(|err| { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); result = error; }); @@ -931,12 +926,7 @@ fn main() -> Result<(), slint::PlatformError> { result = 1 }; - update_order_entries(Some(&to_k), load_orders).unwrap_or_else(|err| { - ui.display_msg(&format!("Failed to parse value to an unsigned int\nError: {err}\n\nResetting load orders")); - result = -1; - std::mem::swap(load_orders, &mut ini::Properties::new()); - }); - load_order.write_to_file().unwrap_or_else(|err| { + load_order.update_order_entries(Some(&to_k)).unwrap_or_else(|err| { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); if !result.is_negative() { result = -1 } }); diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index c865e8c..b338dc4 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -1,4 +1,5 @@ use ini::Ini; +use log::trace; use std::{ collections::HashMap, io::ErrorKind, @@ -142,14 +143,34 @@ impl ModLoaderCfg { self.section().iter() } - /// Returns an owned `HashMap` with values parsed into K: `String`, V: `usize` - pub fn parse_section(&self) -> Result, std::num::ParseIntError> { + /// Returns an owned `HashMap` with values parsed into K: `String`, V: `usize` + /// this function also fixes usize.parse() errors and if values are out of order + pub fn parse_section(&mut self) -> std::io::Result> { + let map = self.parse_into_map(); + if self.section().len() != map.len() { + trace!("fixing usize parse error in \"{}\"", LOADER_FILES[2]); + self.update_order_entries(None)?; + return Ok(self.parse_into_map()); + } + let mut values = self.iter().filter_map(|(k, _)| map.get(k)).collect::>(); + values.sort(); + for (i, value) in values.iter().enumerate() { + if i != **value { + trace!( + "values in \"{}\" are not in order, sorting entries", + LOADER_FILES[2] + ); + self.update_order_entries(None)?; + return Ok(self.parse_into_map()); + } + } + Ok(map) + } + + fn parse_into_map(&self) -> HashMap { self.iter() - .map(|(k, v)| { - let parse_v = v.parse::(); - Ok((k.to_string(), parse_v?)) - }) - .collect::, _>>() + .filter_map(|(k, v)| Some((k.to_string(), v.parse::().ok()?))) + .collect::>() } #[inline] @@ -165,42 +186,42 @@ impl ModLoaderCfg { pub fn write_to_file(&self) -> std::io::Result<()> { self.cfg.write_to_file_opt(&self.cfg_dir, EXT_OPTIONS) } -} -pub fn update_order_entries( - stable: Option<&str>, - section: &mut ini::Properties, -) -> Result<(), std::num::ParseIntError> { - let mut k_v = Vec::with_capacity(section.len()); - let (mut stable_k, mut stable_v) = (String::new(), 0_usize); - for (k, v) in section.clone() { - section.remove(&k); - if let Some(new_k) = stable { - if k == new_k { - (stable_k, stable_v) = (k, v.parse::()?); - continue; + pub fn update_order_entries(&mut self, stable: Option<&str>) -> std::io::Result<()> { + let mut k_v = Vec::with_capacity(self.section().len()); + let (mut stable_k, mut stable_v) = ("", 0_usize); + for (k, v) in self.iter() { + if let Some(new_k) = stable { + if k == new_k { + (stable_k, stable_v) = (k, v.parse::().unwrap_or(usize::MAX)); + continue; + } } + k_v.push((k, v.parse::().unwrap_or(usize::MAX))); } - k_v.push((k, v.parse::()?)); - } - k_v.sort_by_key(|(_, v)| *v); - if k_v.is_empty() && !stable_k.is_empty() { - section.append(&stable_k, "0"); - } else { - let mut offset = 0_usize; - for (k, _) in k_v { - if !stable_k.is_empty() && !section.contains_key(&stable_k) && stable_v == offset { - section.append(&stable_k, stable_v.to_string()); + k_v.sort_by_key(|(_, v)| *v); + + let mut new_section = ini::Properties::new(); + + if k_v.is_empty() && !stable_k.is_empty() { + new_section.append(stable_k, "0"); + } else { + let mut offset = 0_usize; + for (k, _) in k_v { + if !stable_k.is_empty() && stable_v == offset { + new_section.append(std::mem::take(&mut stable_k), stable_v.to_string()); + offset += 1; + } + new_section.append(k, offset.to_string()); offset += 1; } - section.append(k, offset.to_string()); - offset += 1; - } - if !stable_k.is_empty() && !section.contains_key(&stable_k) { - section.append(&stable_k, offset.to_string()) + if !stable_k.is_empty() { + new_section.append(stable_k, offset.to_string()) + } } + std::mem::swap(self.mut_section(), &mut new_section); + self.write_to_file() } - Ok(()) } pub trait Countable { diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 6cb1f17..950187b 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -444,11 +444,13 @@ impl RegMod { } } - // MARK: FIXME? + // MARK: FIXME // when is the best time to verify parsed data? currently we verify data after shaping it // the code would most likely be cleaner if we verified it apon parsing before doing any shaping // should we have two collections? one for deserialization(full) one for just collect and verify + + // collect needs to be completely recoverable, runing into an error and then returning a default is not good enough pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { type CollectedMaps<'a> = (HashMap<&'a str, &'a str>, HashMap<&'a str, Vec<&'a str>>); type ModData<'a> = Vec<( @@ -593,9 +595,9 @@ impl RegMod { let parsed_data = sync_keys(&ini, ini_path)?; let game_dir = IniProperty::::read(&ini, INI_SECTIONS[1], INI_KEYS[1], false)?.value; - let load_order_parsed = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1])? - .parse_section() - .map_err(|err| err.into_io_error())?; + // parse_section is non critical write error | read_section is also non critical write error + let load_order_parsed = + ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1])?.parse_section()?; let parsed_data = combine_map_data(parsed_data, &load_order_parsed); let mut output = Vec::with_capacity(parsed_data.len()); for (k, s, f, l) in parsed_data { @@ -635,14 +637,6 @@ impl RegMod { } Ok(()) } - - pub fn update_dlls(&mut self, new: Vec) { - self.files.dll = new - } - - pub fn update_state(&mut self, new: bool) { - self.state = new - } } pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { diff --git a/src/utils/installer.rs b/src/utils/installer.rs index a2f5d5b..cfe0a86 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -8,14 +8,13 @@ use std::{ use crate::{ does_dir_contain, file_name_or_err, new_io_error, parent_or_err, utils::ini::{ + mod_loader::ModLoader, parser::RegMod, - writer::{save_bool, save_path, save_paths}, + writer::{remove_entry, save_bool, save_path, save_paths}, }, FileData, INI_SECTIONS, LOADER_SECTIONS, }; -use super::ini::{mod_loader::ModLoader, writer::remove_entry}; - /// Returns the deepest occurance of a directory that contains at least 1 file /// Use parent_or_err for a direct binding to what is one level up fn get_parent_dir(input: &Path) -> std::io::Result { From 99819f59c4c4f8566b2a6b8fa1f68a723c275ed7 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Fri, 3 May 2024 12:54:08 -0500 Subject: [PATCH 44/62] More test cases added test case to make sure sort by load_order works now we test that toggle_files() is also saving its changes to ini removed a not needed error in ModLoader::properties() added error check to update_order_entries() to make sure the section is set correclty --- benches/data_collection_benchmark.rs | 2 +- src/lib.rs | 2 +- src/main.rs | 6 +- src/utils/ini/mod_loader.rs | 39 ++++++--- src/utils/ini/parser.rs | 8 +- src/utils/ini/writer.rs | 9 +- src/utils/installer.rs | 2 +- tests/test_ini_tools.rs | 118 ++++++++++++++++++++++----- tests/test_lib.rs | 78 +++++++++++++----- 9 files changed, 199 insertions(+), 65 deletions(-) diff --git a/benches/data_collection_benchmark.rs b/benches/data_collection_benchmark.rs index 7a33835..f2f7779 100644 --- a/benches/data_collection_benchmark.rs +++ b/benches/data_collection_benchmark.rs @@ -23,7 +23,7 @@ fn populate_non_valid_ini(len: u32, file: &Path) { save_bool(file, INI_SECTIONS[2], &key, bool_value).unwrap(); if paths.len() > 1 { - save_paths(file, &key, &path_refs).unwrap(); + save_paths(file, INI_SECTIONS[3], &key, &path_refs).unwrap(); } else { save_path(file, INI_SECTIONS[3], &key, paths[0].as_path()).unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index 7efcdf0..800f17c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,7 +157,7 @@ pub fn toggle_files( save_path(save_file, INI_SECTIONS[3], key, path_to_save[0])?; } else { remove_array(save_file, key)?; - save_paths(save_file, key, path_to_save)?; + save_paths(save_file, INI_SECTIONS[3], key, path_to_save)?; } save_bool(save_file, INI_SECTIONS[2], key, state)?; Ok(()) diff --git a/src/main.rs b/src/main.rs index eee5dbf..4ed42ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -// #![windows_subsystem = "windows"] +#![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ @@ -352,7 +352,7 @@ fn main() -> Result<(), slint::PlatformError> { )), 2.. => { let path_refs = files.iter().map(|p| p.as_path()).collect::>(); - results.push(save_paths(current_ini, &format_key, &path_refs)) + results.push(save_paths(current_ini, INI_SECTIONS[3], &format_key, &path_refs)) }, } if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { @@ -587,7 +587,7 @@ fn main() -> Result<(), slint::PlatformError> { } else { results.push(remove_array(current_ini, &found_mod.name)); } - results.push(save_paths(current_ini, &found_mod.name, &new_data_refs)); + results.push(save_paths(current_ini, INI_SECTIONS[3], &found_mod.name, &new_data_refs)); if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { ui.display_msg(&err.to_string()); let _ = remove_entry( diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index b338dc4..ae2f5e3 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -15,7 +15,7 @@ use crate::{ Operation, OperationResult, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, }; -#[derive(Default)] +#[derive(Debug, Default)] pub struct ModLoader { installed: bool, disabled: bool, @@ -46,13 +46,7 @@ impl ModLoader { path: cfg_dir, }) } else { - return new_io_error!( - ErrorKind::InvalidData, - format!( - "Elden Mod Loader is not installed at: {}", - game_dir.display() - ) - ); + Ok(ModLoader::default()) } } Err(err) => Err(err), @@ -81,7 +75,7 @@ impl ModLoader { } } -#[derive(Default)] +#[derive(Debug, Default)] pub struct ModLoaderCfg { cfg: Ini, cfg_dir: PathBuf, @@ -94,6 +88,7 @@ impl ModLoaderCfg { return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); } let cfg_dir = ModLoader::properties(game_dir)?.path; + let mut cfg = get_cfg(&cfg_dir)?; if !cfg.is_setup(&LOADER_SECTIONS) { new_cfg(&cfg_dir)?; @@ -138,6 +133,14 @@ impl ModLoaderCfg { self.cfg.section(self.section.as_ref()).unwrap() } + #[inline] + /// updates the current section, general sections `None` are not supported + pub fn set_section(&mut self, new: Option<&str>) { + if new.is_some() { + self.section = new.map(String::from) + } + } + #[inline] fn iter(&self) -> ini::PropertyIter { self.section().iter() @@ -187,7 +190,23 @@ impl ModLoaderCfg { self.cfg.write_to_file_opt(&self.cfg_dir, EXT_OPTIONS) } + /// updates the load order values in `Some("loadorder")` so they are always `0..` + /// if you want a key's value to remain the unedited you can supply `Some(stable_key)` + /// then writes the updated key values to file + /// + /// error cases: + /// section is not set to "loadorder" + /// fails to write to file pub fn update_order_entries(&mut self, stable: Option<&str>) -> std::io::Result<()> { + if self.section.as_deref() != LOADER_SECTIONS[1] { + return new_io_error!( + ErrorKind::InvalidInput, + format!( + "This function is only supported to modify Section: \"{}\"", + LOADER_SECTIONS[1].unwrap() + ) + ); + } let mut k_v = Vec::with_capacity(self.section().len()); let (mut stable_k, mut stable_v) = ("", 0_usize); for (k, v) in self.iter() { @@ -228,7 +247,7 @@ pub trait Countable { fn order_count(&self) -> usize; } -impl<'a> Countable for &'a [RegMod] { +impl Countable for &[RegMod] { #[inline] fn order_count(&self) -> usize { self.iter().filter(|m| m.order.set).count() diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 950187b..130346e 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -281,7 +281,7 @@ impl IniProperty { } } -#[derive(Default)] +#[derive(Debug, Default)] pub struct RegMod { /// user defined Key in snake_case pub name: String, @@ -296,7 +296,7 @@ pub struct RegMod { pub order: LoadOrder, } -#[derive(Default)] +#[derive(Debug, Default)] pub struct SplitFiles { /// files with extension `.dll` | also possible they end in `.dll.disabled` /// saved as short paths with `game_dir` truncated @@ -311,7 +311,7 @@ pub struct SplitFiles { pub other: Vec, } -#[derive(Default)] +#[derive(Debug, Default)] pub struct LoadOrder { /// if one of `SplitFiles.dll` has a set load_order pub set: bool, @@ -682,7 +682,7 @@ pub trait ModError { impl ModError for std::io::Error { fn add_msg(self, msg: String) -> std::io::Error { - std::io::Error::new(self.kind(), format!("{msg}\n\nError: {self}")) + std::io::Error::new(self.kind(), format!("{msg}\n\n{self}")) } } diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 23fcae7..7263322 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -23,7 +23,12 @@ pub const EXT_OPTIONS: WriteOption = WriteOption { kv_separator: " = ", }; -pub fn save_paths(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Result<()> { +pub fn save_paths( + file_name: &Path, + section: Option<&str>, + key: &str, + files: &[&Path], +) -> std::io::Result<()> { let mut config: Ini = get_cfg(file_name)?; let save_paths = files .iter() @@ -31,7 +36,7 @@ pub fn save_paths(file_name: &Path, key: &str, files: &[&Path]) -> std::io::Resu .collect::>() .join("\r\narray[]="); config - .with_section(INI_SECTIONS[3]) + .with_section(section) .set(key, format!("array\r\narray[]={save_paths}")); config.write_to_file_opt(file_name, WRITE_OPTIONS) } diff --git a/src/utils/installer.rs b/src/utils/installer.rs index cfe0a86..b8cbaef 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -572,7 +572,7 @@ pub fn scan_for_mods(game_dir: &Path, ini_file: &Path) -> std::io::Result if file_refs.len() == 1 { save_path(ini_file, INI_SECTIONS[3], &mod_data.name, file_refs[0])?; } else { - save_paths(ini_file, &mod_data.name, &file_refs)?; + save_paths(ini_file, INI_SECTIONS[3], &mod_data.name, &file_refs)?; } mod_data.verify_state(game_dir, ini_file)?; } diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index b22d6a8..0d4bda5 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -1,31 +1,36 @@ -#[cfg(test)] +pub mod common; + mod tests { use std::{ - fs::remove_file, + fs::{remove_file, File}, path::{Path, PathBuf}, }; use elden_mod_loader_gui::{ get_cfg, utils::ini::{ + mod_loader::{Countable, ModLoaderCfg}, parser::{IniProperty, RegMod, Setup}, writer::*, }, - INI_KEYS, INI_SECTIONS, OFF_STATE, + INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, OFF_STATE, }; + use crate::common::new_cfg_with_sections; + const GAME_DIR: &str = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; #[test] fn does_u32_parse() { let test_nums: [u32; 3] = [2342652342, 2343523423, 69420]; let test_file = Path::new("temp\\test_nums.ini"); + let test_section = [Some("u32s")]; - new_cfg(test_file).unwrap(); + new_cfg_with_sections(test_file, &test_section).unwrap(); for (i, num) in test_nums.iter().enumerate() { save_value_ext( test_file, - INI_SECTIONS[1], + test_section[0], &format!("test_num_{i}"), &num.to_string(), ) @@ -37,7 +42,7 @@ mod tests { for (i, num) in test_nums.iter().enumerate() { assert_eq!( *num, - IniProperty::::read(&config, INI_SECTIONS[1], &format!("test_num_{i}"), false) + IniProperty::::read(&config, test_section[0], &format!("test_num_{i}"), false) .unwrap() .value ) @@ -51,12 +56,13 @@ mod tests { let test_bools: [&str; 6] = [" True ", "false", "faLSe", "0 ", "0", "1"]; let bool_results: [bool; 6] = [true, false, false, false, false, true]; let test_file = Path::new("temp\\test_bools.ini"); + let test_section = [Some("bools")]; - new_cfg(test_file).unwrap(); + new_cfg_with_sections(test_file, &test_section).unwrap(); for (i, bool_str) in test_bools.iter().enumerate() { save_value_ext( test_file, - INI_SECTIONS[1], + test_section[0], &format!("test_bool_{i}"), bool_str, ) @@ -70,7 +76,7 @@ mod tests { *bool, IniProperty::::read( &config, - INI_SECTIONS[1], + test_section[0], &format!("test_bool_{i}"), false ) @@ -87,20 +93,21 @@ mod tests { let test_path_1 = Path::new(GAME_DIR); let test_path_2 = Path::new("C:\\Windows\\System32"); let test_file = Path::new("temp\\test_path.ini"); + let test_section = [Some("path")]; { - new_cfg(test_file).unwrap(); - save_path(test_file, INI_SECTIONS[1], INI_KEYS[1], test_path_1).unwrap(); - save_path(test_file, INI_SECTIONS[1], "random_dir", test_path_2).unwrap(); + new_cfg_with_sections(test_file, &test_section).unwrap(); + save_path(test_file, test_section[0], INI_KEYS[1], test_path_1).unwrap(); + save_path(test_file, test_section[0], "random_dir", test_path_2).unwrap(); } let config = get_cfg(test_file).unwrap(); let parse_test_1 = - IniProperty::::read(&config, INI_SECTIONS[1], INI_KEYS[1], false) + IniProperty::::read(&config, test_section[0], INI_KEYS[1], false) .unwrap() .value; let parse_test_2 = - IniProperty::::read(&config, INI_SECTIONS[1], "random_dir", false) + IniProperty::::read(&config, test_section[0], "random_dir", false) .unwrap() .value; @@ -110,6 +117,62 @@ mod tests { remove_file(test_file).unwrap(); } + #[test] + fn test_sort_by_order() { + let test_keys = ["a_mod", "b_mod", "c_mod", "d_mod", "f_mod", "e_mod"]; + let test_files = test_keys + .iter() + .map(|k| PathBuf::from(format!("{k}.dll"))) + .collect::>(); + let test_values = ["69420", "2", "1", "0"]; + let sorted_order = ["d_mod", "c_mod", "b_mod", "a_mod", "e_mod", "f_mod"]; + let test_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[2])); + let temp_path = + Path::new("C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui\\temp"); + let test_sections = [LOADER_SECTIONS[1], LOADER_SECTIONS[1], Some("paths")]; + let required_file = LOADER_FILES[1]; + { + new_cfg_with_sections(test_file.as_path(), &test_sections).unwrap(); + for (i, key) in test_keys.iter().enumerate() { + save_path(test_file.as_path(), test_sections[2], key, &test_files[i]).unwrap(); + } + for (i, value) in test_values.iter().enumerate() { + save_value_ext( + test_file.as_path(), + test_sections[1], + test_files[i].to_str().unwrap(), + value, + ) + .unwrap(); + } + File::create(temp_path.join(required_file)).unwrap(); + } + + let mut cfg = ModLoaderCfg::read_section(temp_path, test_sections[1]).unwrap(); + + let parsed_cfg = cfg.parse_section().unwrap(); + + cfg.update_order_entries(None).unwrap(); + let order = test_keys + .iter() + .enumerate() + .map(|(i, key)| { + RegMod::with_load_order(key, true, vec![test_files[i].clone()], &parsed_cfg) + }) + .collect::>(); + + // this tests to make sure the two without an order set are marked as order.set = false + assert_eq!(order.as_slice().order_count(), test_values.len()); + + // this tests that the order is set correclty for the mods that have a order entry + order + .iter() + .filter(|m| m.order.set) + .for_each(|m| assert_eq!(m.name, sorted_order[m.order.at])); + + remove_file(test_file).unwrap(); + } + #[test] #[allow(unused_variables)] fn type_check() { @@ -119,10 +182,12 @@ mod tests { Path::new("mods\\UnlockTheFps\\config.ini"), ]; let test_file = Path::new("temp\\test_type_check.ini"); + let test_sections = [Some("path"), Some("paths")]; + let array_key = "test_array"; new_cfg(test_file).unwrap(); - save_path(test_file, INI_SECTIONS[1], INI_KEYS[1], test_path).unwrap(); - save_paths(test_file, "test_array", &test_array).unwrap(); + save_path(test_file, test_sections[0], INI_KEYS[1], test_path).unwrap(); + save_paths(test_file, test_sections[1], array_key, &test_array).unwrap(); let config = get_cfg(test_file).unwrap(); @@ -136,14 +201,13 @@ mod tests { ); let vec_result = - IniProperty::>::read(&config, INI_SECTIONS[1], INI_KEYS[1], false); + IniProperty::>::read(&config, test_sections[0], INI_KEYS[1], false); assert_eq!( vec_result.unwrap_err().to_string(), vec_pathbuf_err.to_string() ); - let path_result = - IniProperty::::read(&config, INI_SECTIONS[3], "test_array", false); + let path_result = IniProperty::::read(&config, test_sections[1], array_key, false); assert_eq!( path_result.unwrap_err().to_string(), pathbuf_err.to_string() @@ -182,7 +246,13 @@ mod tests { // -----------------------parser is set up to only parse valid entries--------------------------- // ----use case for entries in Section("mod-files") is to keep track of files within game_dir---- - save_paths(test_file, &test_mod_1.name, &test_mod_1.files.file_refs()).unwrap(); + save_paths( + test_file, + INI_SECTIONS[3], + &test_mod_1.name, + &test_mod_1.files.file_refs(), + ) + .unwrap(); save_bool( test_file, INI_SECTIONS[2], @@ -204,7 +274,13 @@ mod tests { test_mod_2.state, ) .unwrap(); - save_paths(test_file, "no_matching_state_1", &invalid_format_1).unwrap(); + save_paths( + test_file, + INI_SECTIONS[3], + "no_matching_state_1", + &invalid_format_1, + ) + .unwrap(); save_path( test_file, INI_SECTIONS[3], diff --git a/tests/test_lib.rs b/tests/test_lib.rs index 148e324..a4ee17f 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -1,40 +1,47 @@ +pub mod common; + #[cfg(test)] mod tests { use elden_mod_loader_gui::{ - toggle_files, - utils::ini::{parser::RegMod, writer::new_cfg}, - OFF_STATE, + get_cfg, toggle_files, + utils::ini::{ + parser::{IniProperty, RegMod}, + writer::{new_cfg, save_path, save_paths}, + }, + INI_KEYS, INI_SECTIONS, OFF_STATE, }; use std::{ - fs::{metadata, remove_file, File}, + fs::{remove_file, File}, path::{Path, PathBuf}, }; + use crate::common::file_exists; + #[test] fn do_files_toggle() { - fn file_exists(file_path: &Path) -> bool { - if let Ok(metadata) = metadata(file_path) { - metadata.is_file() - } else { - false - } - } - let dir_to_test_files = Path::new("C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui"); let save_file = Path::new("temp\\file_toggle_test.ini"); - new_cfg(save_file).unwrap(); let test_files = vec![ - PathBuf::from("temp\\test1.txt"), - PathBuf::from("temp\\test2.bhd"), - PathBuf::from("temp\\test3.dll"), - PathBuf::from("temp\\test4.exe"), - PathBuf::from("temp\\test5.bin"), - PathBuf::from("temp\\config.ini"), + Path::new("temp\\test1.txt"), + Path::new("temp\\test2.bhd"), + Path::new("temp\\test3.dll"), + Path::new("temp\\test4.exe"), + Path::new("temp\\test5.bin"), + Path::new("temp\\config.ini"), ]; + let test_key = "test_files"; - let test_mod = RegMod::new("Test", true, test_files.clone()); + new_cfg(save_file).unwrap(); + save_path(save_file, INI_SECTIONS[1], INI_KEYS[1], dir_to_test_files).unwrap(); + save_paths(save_file, INI_SECTIONS[3], test_key, &test_files).unwrap(); + + let test_mod = RegMod::new( + test_key, + true, + test_files.iter().map(PathBuf::from).collect(), + ); let mut test_files_disabled = test_mod .files .dll @@ -64,6 +71,20 @@ mod tests { test_files_disabled.extend(test_mod.files.config); test_files_disabled.extend(test_mod.files.other); + + let read_disabled_ini = IniProperty::>::read( + &get_cfg(save_file).unwrap(), + INI_SECTIONS[3], + test_key, + true, + ) + .unwrap() + .value; + + assert!(read_disabled_ini + .iter() + .all(|read| test_files_disabled.contains(read))); + let test_mod = RegMod::new(&test_mod.name, false, test_files_disabled); toggle_files( @@ -75,11 +96,24 @@ mod tests { .unwrap(); for path_to_test in test_files.iter() { - assert!(file_exists(path_to_test.as_path())); + assert!(file_exists(path_to_test)); } + let read_enabled_ini = IniProperty::>::read( + &get_cfg(save_file).unwrap(), + INI_SECTIONS[3], + test_key, + true, + ) + .unwrap() + .value; + + assert!(read_enabled_ini + .iter() + .all(|read| test_files.contains(&read.as_path()))); + for test_file in test_files.iter() { - remove_file(test_file.as_path()).unwrap(); + remove_file(test_file).unwrap(); } remove_file(save_file).unwrap(); } From a1822af76d74123e6d9e2d1d910dc09869f4a6f6 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Fri, 3 May 2024 13:02:26 -0500 Subject: [PATCH 45/62] common functions for tests now have their own file --- tests/common.rs | 25 +++++++++++++++++++++++++ tests/test_ini_tools.rs | 1 + 2 files changed, 26 insertions(+) create mode 100644 tests/common.rs diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..34f04c7 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,25 @@ +use std::{ + fs::{create_dir_all, metadata, File}, + io::Write, + path::Path, +}; + +pub fn new_cfg_with_sections(path: &Path, sections: &[Option<&str>]) -> std::io::Result<()> { + let parent = path.parent().unwrap(); + + create_dir_all(parent)?; + let mut new_ini = File::create(path)?; + + for section in sections.iter() { + writeln!(new_ini, "[{}]", section.unwrap())?; + } + Ok(()) +} + +pub fn file_exists(file_path: &Path) -> bool { + if let Ok(metadata) = metadata(file_path) { + metadata.is_file() + } else { + false + } +} diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 0d4bda5..aca384f 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -1,5 +1,6 @@ pub mod common; +#[cfg(test)] mod tests { use std::{ fs::{remove_file, File}, From 7f3036ded4ef1db274bc6af53d89c95488b24d15 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Fri, 3 May 2024 13:30:16 -0500 Subject: [PATCH 46/62] Test for does_dir_contain() --- tests/common.rs | 3 +++ tests/test_ini_tools.rs | 7 ++----- tests/test_lib.rs | 39 +++++++++++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/tests/common.rs b/tests/common.rs index 34f04c7..32f71e0 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -4,6 +4,9 @@ use std::{ path::Path, }; +pub const GAME_DIR: &str = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; +pub const TEMP_DIR: &str = "C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui\\temp"; + pub fn new_cfg_with_sections(path: &Path, sections: &[Option<&str>]) -> std::io::Result<()> { let parent = path.parent().unwrap(); diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index aca384f..9ead385 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -17,9 +17,7 @@ mod tests { INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, OFF_STATE, }; - use crate::common::new_cfg_with_sections; - - const GAME_DIR: &str = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; + use crate::common::{new_cfg_with_sections, GAME_DIR, TEMP_DIR}; #[test] fn does_u32_parse() { @@ -128,8 +126,7 @@ mod tests { let test_values = ["69420", "2", "1", "0"]; let sorted_order = ["d_mod", "c_mod", "b_mod", "a_mod", "e_mod", "f_mod"]; let test_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[2])); - let temp_path = - Path::new("C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui\\temp"); + let temp_path = Path::new(TEMP_DIR); let test_sections = [LOADER_SECTIONS[1], LOADER_SECTIONS[1], Some("paths")]; let required_file = LOADER_FILES[1]; { diff --git a/tests/test_lib.rs b/tests/test_lib.rs index a4ee17f..cd4f7c7 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -3,19 +3,19 @@ pub mod common; #[cfg(test)] mod tests { use elden_mod_loader_gui::{ - get_cfg, toggle_files, + does_dir_contain, get_cfg, toggle_files, utils::ini::{ parser::{IniProperty, RegMod}, writer::{new_cfg, save_path, save_paths}, }, - INI_KEYS, INI_SECTIONS, OFF_STATE, + Operation, OperationResult, INI_KEYS, INI_SECTIONS, OFF_STATE, }; use std::{ - fs::{remove_file, File}, + fs::{self, remove_file, File}, path::{Path, PathBuf}, }; - use crate::common::file_exists; + use crate::common::{file_exists, GAME_DIR}; #[test] fn do_files_toggle() { @@ -117,4 +117,35 @@ mod tests { } remove_file(save_file).unwrap(); } + + #[test] + #[allow(unused_variables)] + fn does_dir_contain_work() { + let mods_dir = PathBuf::from(&format!("{GAME_DIR}\\mods")); + let entries = fs::read_dir(&mods_dir) + .unwrap() + .map(|f| f.unwrap().file_name().into_string().unwrap()) + .collect::>(); + let num_entries = entries.len(); + + assert!(matches!( + does_dir_contain( + &mods_dir, + Operation::Count, + entries.iter().map(|f| f.as_str()).collect::>().as_slice() + ) + .unwrap(), + OperationResult::Count((num_entries, _)) + )); + + assert!(matches!( + does_dir_contain(&mods_dir, Operation::Any, &[entries[1].as_str()]).unwrap(), + OperationResult::Bool(true) + )); + + assert!(matches!( + does_dir_contain(&mods_dir, Operation::Any, &["this_should_not_exist"]).unwrap(), + OperationResult::Bool(false) + )); + } } From 6b888f667664c715095853f43d894ff851d1576e Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Fri, 3 May 2024 15:11:24 -0500 Subject: [PATCH 47/62] cleaned up test --- tests/test_lib.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_lib.rs b/tests/test_lib.rs index cd4f7c7..073d3d1 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -132,20 +132,19 @@ mod tests { does_dir_contain( &mods_dir, Operation::Count, - entries.iter().map(|f| f.as_str()).collect::>().as_slice() - ) - .unwrap(), - OperationResult::Count((num_entries, _)) + entries.iter().map(|e| e.as_ref()).collect::>().as_slice() + ), + Ok(OperationResult::Count((num_entries, _))) )); assert!(matches!( - does_dir_contain(&mods_dir, Operation::Any, &[entries[1].as_str()]).unwrap(), - OperationResult::Bool(true) + does_dir_contain(&mods_dir, Operation::Any, &[&entries[1]]), + Ok(OperationResult::Bool(true)) )); assert!(matches!( - does_dir_contain(&mods_dir, Operation::Any, &["this_should_not_exist"]).unwrap(), - OperationResult::Bool(false) + does_dir_contain(&mods_dir, Operation::Any, &["this_should_not_exist"]), + Ok(OperationResult::Bool(false)) )); } } From f50daa90dd03f2e993625850eb046aac5e4a0f9f Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sat, 4 May 2024 14:12:50 -0500 Subject: [PATCH 48/62] Cleaned up tests --- src/lib.rs | 4 ++-- src/utils/ini/writer.rs | 36 ++++++++++++++++++------------------ tests/common.rs | 1 - tests/test_ini_tools.rs | 23 +++++++++++------------ tests/test_lib.rs | 34 ++++++++++------------------------ 5 files changed, 41 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 800f17c..e610913 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -193,8 +193,8 @@ pub fn toggle_files( Ok(short_path_new) } -pub fn get_cfg(input_file: &Path) -> std::io::Result { - Ini::load_from_file_noescape(input_file).map_err(|err| err.into_io_error()) +pub fn get_cfg(from_path: &Path) -> std::io::Result { + Ini::load_from_file_noescape(from_path).map_err(|err| err.into_io_error()) } pub enum Operation { diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 7263322..37354f7 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -24,12 +24,12 @@ pub const EXT_OPTIONS: WriteOption = WriteOption { }; pub fn save_paths( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, files: &[&Path], ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; let save_paths = files .iter() .map(|path| path.to_string_lossy()) @@ -38,42 +38,42 @@ pub fn save_paths( config .with_section(section) .set(key, format!("array\r\narray[]={save_paths}")); - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } pub fn save_path( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, path: &Path, ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; config .with_section(section) .set(key, path.to_string_lossy().to_string()); - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } pub fn save_bool( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, value: bool, ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; config.with_section(section).set(key, value.to_string()); - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } pub fn save_value_ext( - file_name: &Path, + file_path: &Path, section: Option<&str>, key: &str, value: &str, ) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; + let mut config: Ini = get_cfg(file_path)?; config.with_section(section).set(key, value); - config.write_to_file_opt(file_name, EXT_OPTIONS) + config.write_to_file_opt(file_path, EXT_OPTIONS) } pub fn new_cfg(path: &Path) -> std::io::Result<()> { @@ -103,8 +103,8 @@ pub fn new_cfg(path: &Path) -> std::io::Result<()> { Ok(()) } -pub fn remove_array(file_name: &Path, key: &str) -> std::io::Result<()> { - let content = read_to_string(file_name)?; +pub fn remove_array(file_path: &Path, key: &str) -> std::io::Result<()> { + let content = read_to_string(file_path)?; let mut skip_next_line = false; let mut key_found = false; @@ -123,11 +123,11 @@ pub fn remove_array(file_name: &Path, key: &str) -> std::io::Result<()> { let lines = content.lines().filter(|&line| filter_lines(line)).collect::>(); - write(file_name, lines.join("\r\n")) + write(file_path, lines.join("\r\n")) } -pub fn remove_entry(file_name: &Path, section: Option<&str>, key: &str) -> std::io::Result<()> { - let mut config: Ini = get_cfg(file_name)?; +pub fn remove_entry(file_path: &Path, section: Option<&str>, key: &str) -> std::io::Result<()> { + let mut config: Ini = get_cfg(file_path)?; config.delete_from(section, key).ok_or(std::io::Error::new( ErrorKind::Other, format!( @@ -135,5 +135,5 @@ pub fn remove_entry(file_name: &Path, section: Option<&str>, key: &str) -> std:: §ion.expect("Passed in section should be valid") ), ))?; - config.write_to_file_opt(file_name, WRITE_OPTIONS) + config.write_to_file_opt(file_path, WRITE_OPTIONS) } diff --git a/tests/common.rs b/tests/common.rs index 32f71e0..cffc56c 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -5,7 +5,6 @@ use std::{ }; pub const GAME_DIR: &str = "C:\\Program Files (x86)\\Steam\\steamapps\\common\\ELDEN RING\\Game"; -pub const TEMP_DIR: &str = "C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui\\temp"; pub fn new_cfg_with_sections(path: &Path, sections: &[Option<&str>]) -> std::io::Result<()> { let parent = path.parent().unwrap(); diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 9ead385..de9b710 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -17,7 +17,7 @@ mod tests { INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, OFF_STATE, }; - use crate::common::{new_cfg_with_sections, GAME_DIR, TEMP_DIR}; + use crate::common::{new_cfg_with_sections, GAME_DIR}; #[test] fn does_u32_parse() { @@ -125,28 +125,29 @@ mod tests { .collect::>(); let test_values = ["69420", "2", "1", "0"]; let sorted_order = ["d_mod", "c_mod", "b_mod", "a_mod", "e_mod", "f_mod"]; + let test_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[2])); - let temp_path = Path::new(TEMP_DIR); + let required_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[1])); + let test_sections = [LOADER_SECTIONS[1], LOADER_SECTIONS[1], Some("paths")]; - let required_file = LOADER_FILES[1]; { - new_cfg_with_sections(test_file.as_path(), &test_sections).unwrap(); + new_cfg_with_sections(&test_file, &test_sections).unwrap(); for (i, key) in test_keys.iter().enumerate() { - save_path(test_file.as_path(), test_sections[2], key, &test_files[i]).unwrap(); + save_path(&test_file, test_sections[2], key, &test_files[i]).unwrap(); } for (i, value) in test_values.iter().enumerate() { save_value_ext( - test_file.as_path(), + &test_file, test_sections[1], test_files[i].to_str().unwrap(), value, ) .unwrap(); } - File::create(temp_path.join(required_file)).unwrap(); + File::create(&required_file).unwrap(); } - let mut cfg = ModLoaderCfg::read_section(temp_path, test_sections[1]).unwrap(); + let mut cfg = ModLoaderCfg::read_section(Path::new("temp\\"), test_sections[1]).unwrap(); let parsed_cfg = cfg.parse_section().unwrap(); @@ -169,16 +170,14 @@ mod tests { .for_each(|m| assert_eq!(m.name, sorted_order[m.order.at])); remove_file(test_file).unwrap(); + remove_file(required_file).unwrap(); } #[test] #[allow(unused_variables)] fn type_check() { let test_path = Path::new(GAME_DIR); - let test_array = [ - Path::new("mods\\UnlockTheFps.dll"), - Path::new("mods\\UnlockTheFps\\config.ini"), - ]; + let test_array = [Path::new("temp\\test"), Path::new("temp\\test")]; let test_file = Path::new("temp\\test_type_check.ini"); let test_sections = [Some("path"), Some("paths")]; let array_key = "test_array"; diff --git a/tests/test_lib.rs b/tests/test_lib.rs index 073d3d1..3f9691d 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -19,22 +19,20 @@ mod tests { #[test] fn do_files_toggle() { - let dir_to_test_files = - Path::new("C:\\Users\\cal_b\\Documents\\School\\code\\elden_mod_loader_gui"); let save_file = Path::new("temp\\file_toggle_test.ini"); let test_files = vec![ - Path::new("temp\\test1.txt"), - Path::new("temp\\test2.bhd"), - Path::new("temp\\test3.dll"), - Path::new("temp\\test4.exe"), - Path::new("temp\\test5.bin"), - Path::new("temp\\config.ini"), + Path::new("test1.txt"), + Path::new("test2.bhd"), + Path::new("test3.dll"), + Path::new("test4.exe"), + Path::new("test5.bin"), + Path::new("config.ini"), ]; let test_key = "test_files"; new_cfg(save_file).unwrap(); - save_path(save_file, INI_SECTIONS[1], INI_KEYS[1], dir_to_test_files).unwrap(); + save_path(save_file, INI_SECTIONS[1], INI_KEYS[1], Path::new("temp\\")).unwrap(); save_paths(save_file, INI_SECTIONS[3], test_key, &test_files).unwrap(); let test_mod = RegMod::new( @@ -54,16 +52,10 @@ mod tests { assert_eq!(test_mod.files.other.len(), 4); for test_file in test_files.iter() { - File::create(test_file.to_string_lossy().to_string()).unwrap(); + File::create(test_file).unwrap(); } - toggle_files( - dir_to_test_files, - !test_mod.state, - &test_mod, - Some(save_file), - ) - .unwrap(); + toggle_files(Path::new(""), !test_mod.state, &test_mod, Some(save_file)).unwrap(); for path_to_test in test_files_disabled.iter() { assert!(file_exists(path_to_test.as_path())); @@ -87,13 +79,7 @@ mod tests { let test_mod = RegMod::new(&test_mod.name, false, test_files_disabled); - toggle_files( - dir_to_test_files, - !test_mod.state, - &test_mod, - Some(save_file), - ) - .unwrap(); + toggle_files(Path::new(""), !test_mod.state, &test_mod, Some(save_file)).unwrap(); for path_to_test in test_files.iter() { assert!(file_exists(path_to_test)); From 2db6c85e27255a6c935a7fcf3c9e06f399d59294 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sat, 4 May 2024 23:20:38 -0500 Subject: [PATCH 49/62] Cargo update --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e5a093..0ccd8d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,9 +560,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" @@ -2869,9 +2869,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", From f1070a63a5de8ed669ff2bf7d618c38ebb28981a Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sat, 4 May 2024 23:20:57 -0500 Subject: [PATCH 50/62] improvements to startup process lots of small bug fixes optimizations --- src/lib.rs | 86 ++++++-------- src/main.rs | 221 ++++++++++++++++++++++-------------- src/utils/ini/mod_loader.rs | 30 +++-- src/utils/ini/parser.rs | 95 +++++++++++----- src/utils/ini/writer.rs | 4 +- tests/test_ini_tools.rs | 27 +++-- tests/test_lib.rs | 8 +- 7 files changed, 272 insertions(+), 199 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e610913..b942864 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,10 +8,10 @@ pub mod utils { } use ini::Ini; -use log::{error, info, trace, warn}; +use log::{info, trace, warn}; use utils::ini::{ - parser::{IniProperty, IntoIoError, RegMod}, - writer::{remove_array, save_bool, save_path, save_paths}, + parser::{IniProperty, IntoIoError, RegMod, Setup}, + writer::{new_cfg, remove_array, save_bool, save_path, save_paths}, }; use std::{ @@ -20,6 +20,7 @@ use std::{ path::{Path, PathBuf}, }; +// changing the order of any of the following consts would not be good const DEFAULT_GAME_DIR: [&str; 6] = [ "Program Files (x86)", "Steam", @@ -193,6 +194,20 @@ pub fn toggle_files( Ok(short_path_new) } +pub fn get_or_setup_cfg(from_path: &Path, sections: &[Option<&str>]) -> std::io::Result { + if let Ok(ini) = get_cfg(from_path) { + if ini.is_setup(sections) { + trace!("{} found, and is already setup", LOADER_FILES[2]); + return Ok(ini); + } + }; + warn!( + "ini: {} is not setup: trying to create new", + from_path.display() + ); + new_cfg(from_path) +} + pub fn get_cfg(from_path: &Path) -> std::io::Result { Ini::load_from_file_noescape(from_path).map_err(|err| err.into_io_error()) } @@ -244,6 +259,21 @@ where } } +pub fn files_not_found<'a, T>(in_path: &Path, list: &'a [&T]) -> std::io::Result> +where + T: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash + ?Sized, + for<'b> &'b str: std::borrow::Borrow, +{ + match does_dir_contain(in_path, Operation::Count, list) { + Ok(OperationResult::Count((c, _))) if c == REQUIRED_GAME_FILES.len() => Ok(Vec::new()), + Ok(OperationResult::Count((_, found_files))) => { + Ok(list.iter().filter(|&&e| !found_files.contains(e)).copied().collect()) + } + Err(err) => Err(err), + _ => unreachable!(), + } +} + pub struct FileData<'a> { pub name: &'a str, pub extension: &'a str, @@ -317,47 +347,10 @@ pub enum PathResult { Partial(PathBuf), None(PathBuf), } -pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { - let config: Ini = match get_cfg(file_name) { - Ok(ini) => { - trace!( - "Success: (attempt_locate_game) Read ini from \"{}\"", - file_name.display() - ); - ini - } - Err(err) => { - error!( - "Failure: (attempt_locate_game) Could not complete. Could not read ini from \"{}\"", - file_name.display() - ); - error!("Error: {err}"); - return Ok(PathResult::None(PathBuf::new())); - } - }; - if let Ok(path) = IniProperty::::read(&config, INI_SECTIONS[1], INI_KEYS[1], false) - .and_then(|ini_property| { - // right now all we do is log this error if we want to bail on the fn we can use the ? operator here - match does_dir_contain(&ini_property.value, Operation::All, &REQUIRED_GAME_FILES) { - Ok(OperationResult::Bool(true)) => Ok(ini_property.value), - Ok(OperationResult::Bool(false)) => { - let err = format!( - "Required Game files not found in:\n\"{}\"", - ini_property.value.display() - ); - error!("{err}"); - new_io_error!(ErrorKind::NotFound, err) - } - Err(err) => { - error!("Error: {err}"); - Err(err) - } - _ => unreachable!(), - } - }) - { +pub fn attempt_locate_game(ini_path: &Path, ini: &Ini) -> std::io::Result { + if let Ok(path) = IniProperty::::read(ini, INI_SECTIONS[1], INI_KEYS[1], false) { info!("Success: \"game_dir\" from ini is valid"); - return Ok(PathResult::Full(path)); + return Ok(PathResult::Full(path.value)); } let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); if matches!( @@ -365,12 +358,7 @@ pub fn attempt_locate_game(file_name: &Path) -> std::io::Result { Ok(OperationResult::Bool(true)) ) { info!("Success: located \"game_dir\" on drive"); - save_path( - file_name, - INI_SECTIONS[1], - INI_KEYS[1], - try_locate.as_path(), - )?; + save_path(ini_path, INI_SECTIONS[1], INI_KEYS[1], try_locate.as_path())?; return Ok(PathResult::Full(try_locate)); } if try_locate.components().count() > 1 { diff --git a/src/main.rs b/src/main.rs index 4ed42ac..601f878 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -#![windows_subsystem = "windows"] +// #![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ @@ -14,8 +14,8 @@ use elden_mod_loader_gui::{ *, }; use i_slint_backend_winit::WinitWindowAccessor; -use log::{error, info, warn}; -use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel}; +use log::{error, info, warn, debug}; +use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel, Timer}; use std::{ ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ atomic::{AtomicU32, Ordering}, @@ -54,55 +54,56 @@ fn main() -> Result<(), slint::PlatformError> { let current_ini = get_ini_dir(); let first_startup: bool; let mut errors= Vec::new(); - let ini_valid = match get_cfg(current_ini) { + let ini = match get_cfg(current_ini) { Ok(ini) => { if ini.is_setup(&INI_SECTIONS) { info!("Config file found at \"{}\"", current_ini.display()); first_startup = false; - true + Some(ini) } else { first_startup = false; - false + None } } Err(err) => { // io::Open error or | parse error with type ErrorKind::InvalidData error!("Error: {err}"); if err.kind() == ErrorKind::InvalidData { + debug!("error 1"); errors.push(err); } first_startup = true; - false + None } }; - if !ini_valid { + let ini = ini.unwrap_or_else(|| { warn!("Ini not setup correctly. Creating new Ini"); - new_cfg(current_ini).unwrap(); - } + new_cfg(current_ini).unwrap_or_else(|err| { + // io::Write error + debug!("error 2"); + errors.push(err); + ini::Ini::new() + }) + }); let game_verified: bool; let mut reg_mods = None; - let game_dir = match attempt_locate_game(current_ini) { + let game_dir = match attempt_locate_game(current_ini, &ini) { Ok(path_result) => match path_result { PathResult::Full(path) => { - reg_mods = Some(RegMod::collect(current_ini, false)); - match reg_mods { - Some(Ok(ref reg_mods)) => { - reg_mods.iter().for_each(|data| { - data.verify_state(&path, current_ini) - // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error - .unwrap_or_else(|err| errors.push(err)) - }); + match RegMod::collect(current_ini, false) { + Ok(mod_data) => { game_verified = true; + reg_mods = Some(mod_data); Some(path) } - Some(Err(ref err)) => { - // io::Write error + Err(err) => { + // io::Write error | PermissionDenied + debug!("error 3"); errors.push(err.clone_err()); game_verified = true; Some(path) } - None => unreachable!() }}, PathResult::Partial(path) | PathResult::None(path) => { game_verified = false; @@ -111,6 +112,7 @@ fn main() -> Result<(), slint::PlatformError> { }, Err(err) => { // io::Write error + debug!("error 4"); errors.push(err); game_verified = false; None @@ -121,16 +123,19 @@ fn main() -> Result<(), slint::PlatformError> { &get_cfg(current_ini).expect("ini file is verified"), INI_SECTIONS[0], INI_KEYS[0], - false, ) { Ok(bool) => ui.global::().set_dark_mode(bool.value), Err(err) => { // io::Read error + debug!("error 5"); errors.push(err); ui.global::().set_dark_mode(true); save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], true) // io::Write error - .unwrap_or_else(|err| errors.push(err)); + .unwrap_or_else(|err| { + debug!("error 6"); + errors.push(err) + }); } }; @@ -150,16 +155,19 @@ fn main() -> Result<(), slint::PlatformError> { ui.global::().set_current_subpage(1); mod_loader = ModLoader::default(); } else { - let game_dir = game_dir.expect("game dir verified"); - mod_loader = ModLoader::properties(&game_dir).unwrap_or_else(|err| { + let game_dir = game_dir.as_ref().expect("game dir verified"); + mod_loader = ModLoader::properties(game_dir).unwrap_or_else(|err| { + debug!("error 7"); errors.push(err); ModLoader::default() }); deserialize_current_mods( - &match reg_mods { - Some(Ok(mod_data)) => mod_data, - _ => RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { + &if let Some(mod_data) = reg_mods { + mod_data + } else { + RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error + debug!("error 8"); errors.push(err); vec![RegMod::default()] }) @@ -169,7 +177,7 @@ fn main() -> Result<(), slint::PlatformError> { if mod_loader.installed() { ui.global::().set_loader_installed(true); - let loader_cfg = ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[0]).unwrap(); + let loader_cfg = ModLoaderCfg::read_section(mod_loader.path(), LOADER_SECTIONS[0]).unwrap(); let delay = loader_cfg.get_load_delay().unwrap_or_else(|_| { // parse error ErrorKind::InvalidData let err = std::io::Error::new(ErrorKind::InvalidData, format!( @@ -177,11 +185,13 @@ fn main() -> Result<(), slint::PlatformError> { LOADER_KEYS[0] )); error!("{err}"); + debug!("error 9"); errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_LOADER_VALUES[0]) .unwrap_or_else(|err| { // io::write error error!("{err}"); + debug!("error 10"); errors.push(err); }); DEFAULT_LOADER_VALUES[0].parse().unwrap() @@ -193,11 +203,13 @@ fn main() -> Result<(), slint::PlatformError> { LOADER_KEYS[1] )); error!("{err}"); + debug!("error 11"); errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_LOADER_VALUES[1]) .unwrap_or_else(|err| { // io::write error error!("{err}"); + debug!("error 12"); errors.push(err); }); false @@ -210,9 +222,8 @@ fn main() -> Result<(), slint::PlatformError> { // we need to wait for slint event loop to start `ui.run()` before making calls to `ui.display_msg()` // otherwise calculations for the positon of display_msg_popup are not correct let ui_handle = ui.as_weak(); - let _ = std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(200)); - slint::invoke_from_event_loop(move || { + slint::invoke_from_event_loop(move || { + Timer::single_shot(std::time::Duration::from_millis(200), move || { slint::spawn_local(async move { let ui = ui_handle.unwrap(); if !errors.is_empty() { @@ -222,43 +233,45 @@ fn main() -> Result<(), slint::PlatformError> { } } if first_startup { - if !game_verified && !mod_loader.installed() { + if !game_verified { ui.display_msg( - "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease select the game directory containing \"eldenring.exe\"", + "Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nPlease select the game directory containing \"eldenring.exe\"", ); } else if game_verified && !mod_loader.installed() { ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!\n\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app"); } else if game_verified { - match confirm_scan_mods(ui.as_weak(), &get_or_update_game_dir(None), current_ini, false).await { - Ok(len) => { - deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), ui.as_weak() - ); - ui.display_msg(&format!("Successfully Found {len} mod(s)")); - let _ = receive_msg().await; - } - Err(err) => if err.kind() != ErrorKind::ConnectionAborted { - ui.display_msg(&format!("Error: {err}")); - let _ = receive_msg().await; - } - }; ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); + let _ = receive_msg().await; + match mods_registered(IniOption::Ini(ini)) { + Ok(n) => { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), current_ini, n).await { + ui.display_msg(&err.to_string()); + }; + } + Err(err) => ui.display_msg(&err.to_string()) + } } } else if game_verified { if !mod_loader.installed() { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", get_or_update_game_dir(None).display())); } + match mods_registered(IniOption::Ini(ini)) { + Ok(0) => { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), current_ini, 0).await { + ui.display_msg(&err.to_string()); + }; + } + Ok(_) => (), + Err(err) => ui.display_msg(&err.to_string()), + } } else { ui.display_msg( "Failed to locate Elden Ring\nPlease Select the install directory for Elden Ring", ); } }).unwrap(); - }).unwrap(); - }); + }); + }).unwrap(); } // TODO: Error check input text for invalid symbols @@ -423,8 +436,8 @@ fn main() -> Result<(), slint::PlatformError> { } _ => unreachable!(), }; - match does_dir_contain(Path::new(&try_path), Operation::All, &REQUIRED_GAME_FILES) { - Ok(OperationResult::Bool(true)) => { + match files_not_found(&try_path, &REQUIRED_GAME_FILES) { + Ok(not_found) => if not_found.is_empty() { let result = save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path); if result.is_err() && save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path).is_err() { let err = result.unwrap_err(); @@ -436,19 +449,27 @@ fn main() -> Result<(), slint::PlatformError> { let mod_loader = ModLoader::properties(&try_path).unwrap_or_default(); ui.global::() .set_game_path(try_path.to_string_lossy().to_string().into()); - let _ = get_or_update_game_dir(Some(try_path)); ui.global::().set_game_path_valid(true); ui.global::().set_current_subpage(0); ui.global::().set_loader_installed(mod_loader.installed()); ui.global::().set_loader_disabled(mod_loader.disabled()); if mod_loader.installed() { - ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!") + ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); + let _ = receive_msg().await; + match mods_registered(IniOption::Path(current_ini)) { + Ok(n) => { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, current_ini, n).await { + ui.display_msg(&err.to_string()); + }; + } + Err(err) => ui.display_msg(&err.to_string()) + } } else { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") } - } - Ok(OperationResult::Bool(false)) => { - let err = format!("Required Game files not found in:\n\"{}\"", try_path.display()); + let _ = get_or_update_game_dir(Some(try_path)); + } else { + let err = format!("{} files not found in:\n\"{}\"", not_found.join("\n"), try_path.display()); error!("{err}"); ui.display_msg(&err); } @@ -459,7 +480,6 @@ fn main() -> Result<(), slint::PlatformError> { } ui.display_msg(&err.to_string()) } - _ => unreachable!(), } }).unwrap(); } @@ -845,25 +865,17 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); slint::spawn_local(async move { + let current_ini = get_ini_dir(); let game_dir = get_or_update_game_dir(None); - match confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, true).await { - Ok(len) => { - ui.global::().set_current_subpage(0); - let mod_loader = ModLoader::properties(&game_dir).unwrap_or_default(); - deserialize_current_mods( - &RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }),ui.as_weak() - ); - ui.display_msg(&format!("Successfully Found {len} mod(s)")); - } - Err(err) => if err.kind() != ErrorKind::ConnectionAborted { - ui.display_msg(&format!("Error: {err}")); + match mods_registered(IniOption::Path(current_ini)) { + Ok(n) => { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, n).await { + ui.display_msg(&err.to_string()); + }; } - }; + Err(err) => ui.display_msg(&err.to_string()) + } }).unwrap(); } }); @@ -872,9 +884,9 @@ fn main() -> Result<(), slint::PlatformError> { move |state, key, value| -> i32 { let ui = ui_handle.unwrap(); let error = 42069_i32; - let game_dir = get_or_update_game_dir(None); + let cfg_dir = get_or_update_game_dir(None).join(LOADER_FILES[2]); let mut result: i32 = if state { 1 } else { -1 }; - let mut load_order = match ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) { + let mut load_order = match ModLoaderCfg::read_section(&cfg_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err.to_string()); @@ -907,8 +919,8 @@ fn main() -> Result<(), slint::PlatformError> { move |to_k, from_k, value| -> i32 { let ui = ui_handle.unwrap(); let mut result = 0_i32; - let game_dir = get_or_update_game_dir(None); - let mut load_order = match ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1]) { + let cfg_dir = get_or_update_game_dir(None).join(LOADER_FILES[2]); + let mut load_order = match ModLoaderCfg::read_section(&cfg_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err.to_string()); @@ -1245,20 +1257,41 @@ async fn confirm_remove_mod( remove_mod_files(game_dir, reg_mod) } +enum IniOption<'a> { + Ini(ini::Ini), + Path(&'a Path), +} + +fn mods_registered(input_ini: IniOption) -> std::io::Result { + let ini = match input_ini { + IniOption::Ini(ini) => ini, + IniOption::Path(ini_file) => { + match get_or_setup_cfg(ini_file, &INI_SECTIONS) { + Ok(ini) => ini, + Err(err) => return Err(err), + } + } + }; + let empty_ini = ini.section(INI_SECTIONS[2]).is_none() || ini.section(INI_SECTIONS[2]).unwrap().is_empty(); + if empty_ini { Ok(0) } else { Ok(ini.section(INI_SECTIONS[2]).unwrap().len()) } +} + async fn confirm_scan_mods( ui_weak: slint::Weak, game_dir: &Path, ini_file: &Path, - ini_exists: bool) -> std::io::Result { + mods_registered: usize) -> std::io::Result<()>{ let ui = ui_weak.unwrap(); + ui.display_confirm("Would you like to attempt to auto-import already installed mods to Elden Mod Loader GUI?", true); if receive_msg().await != Message::Confirm { - return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to scan for mods"); + return Ok(()); }; - if ini_exists { + let empty_ini = mods_registered == 0; + if !empty_ini { ui.display_confirm("Warning: This action will reset current registered mods, are you sure you want to continue?", true); if receive_msg().await != Message::Confirm { - return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to scan for mods"); + return Ok(()); }; let dark_mode = ui.global::().get_dark_mode(); // MARK: TODO @@ -1268,5 +1301,21 @@ async fn confirm_scan_mods( save_bool(ini_file, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; save_path(ini_file, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; } - scan_for_mods(game_dir, ini_file) + match scan_for_mods(game_dir, ini_file) { + Ok(len) => { + ui.global::().set_current_subpage(0); + let mod_loader = ModLoader::properties(game_dir).unwrap_or_default(); + deserialize_current_mods( + &RegMod::collect(ini_file, !mod_loader.installed()).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }),ui.as_weak() + ); + ui.display_msg(&format!("Successfully Found {len} mod(s)")); + } + Err(err) => if err.kind() != ErrorKind::ConnectionAborted { + ui.display_msg(&format!("Error: {err}")); + } + }; + Ok(()) } \ No newline at end of file diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index ae2f5e3..f492d4c 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -1,5 +1,5 @@ use ini::Ini; -use log::trace; +use log::{trace, warn}; use std::{ collections::HashMap, io::ErrorKind, @@ -7,9 +7,9 @@ use std::{ }; use crate::{ - does_dir_contain, get_cfg, new_io_error, + does_dir_contain, get_or_setup_cfg, new_io_error, utils::ini::{ - parser::{IniProperty, ModError, RegMod, Setup}, + parser::{IniProperty, ModError, RegMod}, writer::{new_cfg, EXT_OPTIONS}, }, Operation, OperationResult, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, @@ -27,7 +27,8 @@ impl ModLoader { let cfg_dir = game_dir.join(LOADER_FILES[2]); match does_dir_contain(game_dir, Operation::Count, &LOADER_FILES) { Ok(OperationResult::Count((_, files))) => { - if files.contains(LOADER_FILES[1]) || !files.contains(LOADER_FILES[0]) { + if files.contains(LOADER_FILES[1]) && !files.contains(LOADER_FILES[0]) { + trace!("Mod loader found in the Enabled state"); if !files.contains(LOADER_FILES[2]) { new_cfg(&cfg_dir)?; } @@ -36,7 +37,8 @@ impl ModLoader { disabled: false, path: cfg_dir, }) - } else if files.contains(LOADER_FILES[0]) || !files.contains(LOADER_FILES[1]) { + } else if files.contains(LOADER_FILES[0]) && !files.contains(LOADER_FILES[1]) { + trace!("Mod loader found in the Disabled state"); if !files.contains(LOADER_FILES[2]) { new_cfg(&cfg_dir)?; } @@ -46,6 +48,7 @@ impl ModLoader { path: cfg_dir, }) } else { + warn!("Mod loader dll hook not found"); Ok(ModLoader::default()) } } @@ -83,28 +86,21 @@ pub struct ModLoaderCfg { } impl ModLoaderCfg { - pub fn read_section(game_dir: &Path, section: Option<&str>) -> std::io::Result { + pub fn read_section(cfg_dir: &Path, section: Option<&str>) -> std::io::Result { if section.is_none() { return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); } - let cfg_dir = ModLoader::properties(game_dir)?.path; - let mut cfg = get_cfg(&cfg_dir)?; - if !cfg.is_setup(&LOADER_SECTIONS) { - new_cfg(&cfg_dir)?; - } - if cfg.section(section).is_none() { - cfg.init_section(section)? - } + let cfg = get_or_setup_cfg(cfg_dir, &LOADER_SECTIONS)?; Ok(ModLoaderCfg { cfg, - cfg_dir, + cfg_dir: PathBuf::from(cfg_dir), section: section.map(String::from), }) } pub fn get_load_delay(&self) -> std::io::Result { - match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0], false) { + match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0]) { Ok(delay_time) => Ok(delay_time.value), Err(err) => Err(err.add_msg(format!( "Found an unexpected character saved in \"{}\"", @@ -114,7 +110,7 @@ impl ModLoaderCfg { } pub fn get_show_terminal(&self) -> std::io::Result { - match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1], false) { + match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1]) { Ok(delay_time) => Ok(delay_time.value), Err(err) => Err(err.add_msg(format!( "Found an unexpected character saved in \"{}\"", diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 130346e..896a04c 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -7,29 +7,30 @@ use std::{ }; use crate::{ - get_cfg, new_io_error, toggle_files, + files_not_found, get_cfg, new_io_error, toggle_files, utils::ini::{ mod_loader::ModLoaderCfg, writer::{remove_array, remove_entry}, }, - FileData, ARRAY_KEY, ARRAY_VALUE, INI_KEYS, INI_SECTIONS, LOADER_SECTIONS, OFF_STATE, + FileData, ARRAY_KEY, ARRAY_VALUE, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, + OFF_STATE, REQUIRED_GAME_FILES, }; pub trait Parsable: Sized { - fn parse_str>( + fn parse_str( ini: &Ini, section: Option<&str>, - partial_path: Option, + partial_path: Option<&Path>, key: &str, skip_validation: bool, ) -> std::io::Result; } impl Parsable for bool { - fn parse_str>( + fn parse_str( ini: &Ini, section: Option<&str>, - _partial_path: Option, + _partial_path: Option<&Path>, key: &str, _skip_validation: bool, ) -> std::io::Result { @@ -45,10 +46,10 @@ impl Parsable for bool { } impl Parsable for u32 { - fn parse_str>( + fn parse_str( ini: &Ini, section: Option<&str>, - _partial_path: Option, + _partial_path: Option<&Path>, key: &str, _skip_validation: bool, ) -> std::io::Result { @@ -60,10 +61,10 @@ impl Parsable for u32 { } impl Parsable for PathBuf { - fn parse_str>( + fn parse_str( ini: &Ini, section: Option<&str>, - partial_path: Option, + partial_path: Option<&Path>, key: &str, skip_validation: bool, ) -> std::io::Result { @@ -81,15 +82,25 @@ impl Parsable for PathBuf { return Ok(parsed_value); } parsed_value.as_path().validate(partial_path)?; + if key == INI_KEYS[1] { + match files_not_found(&parsed_value, &REQUIRED_GAME_FILES) { + Ok(not_found) => { + if !not_found.is_empty() { + return new_io_error!(ErrorKind::NotFound, format!("Could not verify the install directory of Elden Ring, the following files were not found: \n{}", not_found.join("\n"))); + } + } + Err(err) => return Err(err), + } + } Ok(parsed_value) } } impl Parsable for Vec { - fn parse_str>( + fn parse_str( ini: &Ini, section: Option<&str>, - partial_path: Option, + partial_path: Option<&Path>, key: &str, skip_validation: bool, ) -> std::io::Result { @@ -230,41 +241,66 @@ pub struct IniProperty { pub value: T, } -impl IniProperty { +impl IniProperty { + pub fn read(ini: &Ini, section: Option<&str>, key: &str) -> std::io::Result> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, false, None)?, + }) + } +} +impl IniProperty { + pub fn read(ini: &Ini, section: Option<&str>, key: &str) -> std::io::Result> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, false, None)?, + }) + } +} +impl IniProperty { pub fn read( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, - ) -> std::io::Result> { + ) -> std::io::Result> { Ok(IniProperty { //section: section.map(String::from), //key: key.to_string(), - value: IniProperty::is_valid(ini, section, key, skip_validation)?, + value: IniProperty::is_valid(ini, section, key, skip_validation, None)?, }) } +} + +impl IniProperty> { + pub fn read( + ini: &Ini, + section: Option<&str>, + key: &str, + path_prefix: &Path, + skip_validation: bool, + ) -> std::io::Result>> { + Ok(IniProperty { + //section: section.map(String::from), + //key: key.to_string(), + value: IniProperty::is_valid(ini, section, key, skip_validation, Some(path_prefix))?, + }) + } +} +impl IniProperty { fn is_valid( ini: &Ini, section: Option<&str>, key: &str, skip_validation: bool, + path_prefix: Option<&Path>, ) -> std::io::Result { match &ini.section(section) { Some(s) => match s.contains_key(key) { - true => { - // This will have to be abstracted to the caller if we want the ability for the caller to specify the _path_prefix_ - // right now _game_dir_ is the only valid prefix && "mod-files" is the only place _short_paths_ are stored - let game_dir = if section == INI_SECTIONS[3] { - Some( - IniProperty::::read(ini, INI_SECTIONS[1], INI_KEYS[1], false)? - .value, - ) - } else { - None - }; - T::parse_str(ini, section, game_dir, key, skip_validation) - } + true => T::parse_str(ini, section, path_prefix, key, skip_validation), false => new_io_error!( ErrorKind::NotFound, format!("Key: \"{key}\" not found in {ini:?}") @@ -597,7 +633,8 @@ impl RegMod { IniProperty::::read(&ini, INI_SECTIONS[1], INI_KEYS[1], false)?.value; // parse_section is non critical write error | read_section is also non critical write error let load_order_parsed = - ModLoaderCfg::read_section(&game_dir, LOADER_SECTIONS[1])?.parse_section()?; + ModLoaderCfg::read_section(&game_dir.join(LOADER_FILES[2]), LOADER_SECTIONS[1])? + .parse_section()?; let parsed_data = combine_map_data(parsed_data, &load_order_parsed); let mut output = Vec::with_capacity(parsed_data.len()); for (k, s, f, l) in parsed_data { diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 37354f7..b4093ad 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -76,7 +76,7 @@ pub fn save_value_ext( config.write_to_file_opt(file_path, EXT_OPTIONS) } -pub fn new_cfg(path: &Path) -> std::io::Result<()> { +pub fn new_cfg(path: &Path) -> std::io::Result { let file_name = file_name_or_err(path)?; let parent = parent_or_err(path)?; @@ -100,7 +100,7 @@ pub fn new_cfg(path: &Path) -> std::io::Result<()> { } } } - Ok(()) + get_cfg(path) } pub fn remove_array(file_path: &Path, key: &str) -> std::io::Result<()> { diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index de9b710..5db5a9f 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -41,7 +41,7 @@ mod tests { for (i, num) in test_nums.iter().enumerate() { assert_eq!( *num, - IniProperty::::read(&config, test_section[0], &format!("test_num_{i}"), false) + IniProperty::::read(&config, test_section[0], &format!("test_num_{i}")) .unwrap() .value ) @@ -73,14 +73,9 @@ mod tests { for (i, bool) in bool_results.iter().enumerate() { assert_eq!( *bool, - IniProperty::::read( - &config, - test_section[0], - &format!("test_bool_{i}"), - false - ) - .unwrap() - .value + IniProperty::::read(&config, test_section[0], &format!("test_bool_{i}")) + .unwrap() + .value ) } @@ -129,7 +124,7 @@ mod tests { let test_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[2])); let required_file = PathBuf::from(&format!("temp\\{}", LOADER_FILES[1])); - let test_sections = [LOADER_SECTIONS[1], LOADER_SECTIONS[1], Some("paths")]; + let test_sections = [LOADER_SECTIONS[0], LOADER_SECTIONS[1], Some("paths")]; { new_cfg_with_sections(&test_file, &test_sections).unwrap(); for (i, key) in test_keys.iter().enumerate() { @@ -147,7 +142,7 @@ mod tests { File::create(&required_file).unwrap(); } - let mut cfg = ModLoaderCfg::read_section(Path::new("temp\\"), test_sections[1]).unwrap(); + let mut cfg = ModLoaderCfg::read_section(&test_file, test_sections[1]).unwrap(); let parsed_cfg = cfg.parse_section().unwrap(); @@ -197,8 +192,13 @@ mod tests { "Invalid type found. Expected: Vec, Found: Path", ); - let vec_result = - IniProperty::>::read(&config, test_sections[0], INI_KEYS[1], false); + let vec_result = IniProperty::>::read( + &config, + test_sections[0], + INI_KEYS[1], + test_path, + false, + ); assert_eq!( vec_result.unwrap_err().to_string(), vec_pathbuf_err.to_string() @@ -318,7 +318,6 @@ mod tests { &get_cfg(test_file).unwrap(), INI_SECTIONS[2], &test_mod_2.name, - false, ) .unwrap() .value; diff --git a/tests/test_lib.rs b/tests/test_lib.rs index 3f9691d..ded2eb1 100644 --- a/tests/test_lib.rs +++ b/tests/test_lib.rs @@ -8,7 +8,7 @@ mod tests { parser::{IniProperty, RegMod}, writer::{new_cfg, save_path, save_paths}, }, - Operation, OperationResult, INI_KEYS, INI_SECTIONS, OFF_STATE, + Operation, OperationResult, INI_SECTIONS, OFF_STATE, }; use std::{ fs::{self, remove_file, File}, @@ -30,9 +30,11 @@ mod tests { Path::new("config.ini"), ]; let test_key = "test_files"; + let prefix_key = "test_dir"; + let prefix = Path::new("temp\\"); new_cfg(save_file).unwrap(); - save_path(save_file, INI_SECTIONS[1], INI_KEYS[1], Path::new("temp\\")).unwrap(); + save_path(save_file, INI_SECTIONS[1], prefix_key, prefix).unwrap(); save_paths(save_file, INI_SECTIONS[3], test_key, &test_files).unwrap(); let test_mod = RegMod::new( @@ -68,6 +70,7 @@ mod tests { &get_cfg(save_file).unwrap(), INI_SECTIONS[3], test_key, + prefix, true, ) .unwrap() @@ -89,6 +92,7 @@ mod tests { &get_cfg(save_file).unwrap(), INI_SECTIONS[3], test_key, + prefix, true, ) .unwrap() From bbb4e83dcc6b5c0ae761ae3ac7d8b6c01917b248 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 5 May 2024 14:10:48 -0500 Subject: [PATCH 51/62] Cargo update --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ccd8d4..5068da8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5348,18 +5348,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" dependencies = [ "proc-macro2", "quote", From 384575846d158119f2fbabf60611e0dbda6cbb16 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 5 May 2024 14:11:13 -0500 Subject: [PATCH 52/62] removed io Reads from collect_mods() changed the interface for RegMod::collect() to Cfg.collect_mods() this lets us abstract all io reads to the caller, doing so makes the actuall collection much faster and can handle errors better this way we now also abstract the load_order io read to the caller as well and give the option to collect without determing each mods order this was mostly done to reduce and re-use io read data, not having to re-read data when not needed is nice --- benches/data_collection_benchmark.rs | 8 +- src/lib.rs | 90 +++++-- src/main.rs | 384 ++++++++++++++++----------- src/utils/ini/mod_loader.rs | 61 ++--- src/utils/ini/parser.rs | 71 ++--- tests/test_ini_tools.rs | 5 +- 6 files changed, 357 insertions(+), 262 deletions(-) diff --git a/benches/data_collection_benchmark.rs b/benches/data_collection_benchmark.rs index f2f7779..aa70bbe 100644 --- a/benches/data_collection_benchmark.rs +++ b/benches/data_collection_benchmark.rs @@ -1,9 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use elden_mod_loader_gui::{ - utils::ini::{parser::RegMod, writer::*}, - INI_SECTIONS, -}; +use elden_mod_loader_gui::{utils::ini::writer::*, Cfg, INI_SECTIONS}; use rand::{distributions::Alphanumeric, Rng}; use std::{ fs::remove_file, @@ -48,10 +45,11 @@ fn generate_test_paths() -> Vec { fn data_collection_benchmark(c: &mut Criterion) { let test_file = Path::new(BENCH_TEST_FILE); + let ini = Cfg::read(test_file).unwrap(); populate_non_valid_ini(NUM_ENTRIES, test_file); c.bench_function("data_collection", |b| { - b.iter(|| black_box(RegMod::collect(test_file, true))); + b.iter(|| black_box(ini.collect_mods(None, true))); }); remove_file(test_file).unwrap(); } diff --git a/src/lib.rs b/src/lib.rs index b942864..38c5684 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ pub mod utils { } use ini::Ini; -use log::{info, trace, warn}; +use log::{error, info, trace, warn}; use utils::ini::{ parser::{IniProperty, IntoIoError, RegMod, Setup}, writer::{new_cfg, remove_array, save_bool, save_path, save_paths}, @@ -66,6 +66,11 @@ macro_rules! new_io_error { }; } +pub enum IniOption<'a> { + Ini(&'a ini::Ini), + Path(&'a Path), +} + pub struct PathErrors { pub ok_paths_short: Vec, pub err_paths_long: Vec, @@ -195,16 +200,19 @@ pub fn toggle_files( } pub fn get_or_setup_cfg(from_path: &Path, sections: &[Option<&str>]) -> std::io::Result { - if let Ok(ini) = get_cfg(from_path) { - if ini.is_setup(sections) { - trace!("{} found, and is already setup", LOADER_FILES[2]); - return Ok(ini); + match get_cfg(from_path) { + Ok(ini) => { + if ini.is_setup(sections) { + trace!("{:?} found, and is already setup", from_path.file_name()); + return Ok(ini); + } + trace!( + "ini: {:?} is not setup, creating new", + from_path.file_name() + ); } + Err(err) => error!("{err} : {:?}", from_path.file_name()), }; - warn!( - "ini: {} is not setup: trying to create new", - from_path.display() - ); new_cfg(from_path) } @@ -342,31 +350,61 @@ pub fn file_name_or_err(path: &Path) -> std::io::Result<&std::ffi::OsStr> { )) } +#[derive(Debug, Default)] +pub struct Cfg { + pub data: Ini, + pub dir: PathBuf, +} + pub enum PathResult { Full(PathBuf), Partial(PathBuf), None(PathBuf), } -pub fn attempt_locate_game(ini_path: &Path, ini: &Ini) -> std::io::Result { - if let Ok(path) = IniProperty::::read(ini, INI_SECTIONS[1], INI_KEYS[1], false) { - info!("Success: \"game_dir\" from ini is valid"); - return Ok(PathResult::Full(path.value)); + +impl Cfg { + pub fn from(ini: Ini, ini_path: &Path) -> Self { + Cfg { + data: ini, + dir: PathBuf::from(ini_path), + } } - let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); - if matches!( - does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES), - Ok(OperationResult::Bool(true)) - ) { - info!("Success: located \"game_dir\" on drive"); - save_path(ini_path, INI_SECTIONS[1], INI_KEYS[1], try_locate.as_path())?; - return Ok(PathResult::Full(try_locate)); + pub fn read(ini_path: &Path) -> std::io::Result { + let data = get_or_setup_cfg(ini_path, &INI_SECTIONS)?; + Ok(Cfg { + data, + dir: PathBuf::from(ini_path), + }) } - if try_locate.components().count() > 1 { - info!("Partial \"game_dir\" found"); - return Ok(PathResult::Partial(try_locate)); + + pub fn attempt_locate_game(&self) -> std::io::Result { + if let Ok(path) = + IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false) + { + info!("Success: \"game_dir\" from ini is valid"); + return Ok(PathResult::Full(path.value)); + } + let try_locate = attempt_locate_dir(&DEFAULT_GAME_DIR).unwrap_or("".into()); + if matches!( + does_dir_contain(&try_locate, Operation::All, &REQUIRED_GAME_FILES), + Ok(OperationResult::Bool(true)) + ) { + info!("Success: located \"game_dir\" on drive"); + save_path( + &self.dir, + INI_SECTIONS[1], + INI_KEYS[1], + try_locate.as_path(), + )?; + return Ok(PathResult::Full(try_locate)); + } + if try_locate.components().count() > 1 { + info!("Partial \"game_dir\" found"); + return Ok(PathResult::Partial(try_locate)); + } + warn!("Could not locate \"game_dir\""); + Ok(PathResult::None(try_locate)) } - warn!("Could not locate \"game_dir\""); - Ok(PathResult::None(try_locate)) } fn attempt_locate_dir(target_path: &[&str]) -> std::io::Result { diff --git a/src/main.rs b/src/main.rs index 601f878..4889941 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use elden_mod_loader_gui::{ utils::{ ini::{ mod_loader::{ModLoader, ModLoaderCfg, Countable}, - parser::{file_registered, IniProperty, RegMod, Setup, ErrorClone}, + parser::{file_registered, IniProperty, RegMod, Setup}, writer::*, }, installer::{remove_mod_files, InstallData, scan_for_mods} @@ -17,7 +17,7 @@ use i_slint_backend_winit::WinitWindowAccessor; use log::{error, info, warn, debug}; use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel, Timer}; use std::{ - ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ + collections::HashMap, ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ atomic::{AtomicU32, Ordering}, OnceLock, } @@ -35,6 +35,7 @@ static RECEIVER: OnceLock>> = OnceLock::ne fn main() -> Result<(), slint::PlatformError> { env_logger::init(); + slint::platform::set_platform(Box::new( i_slint_backend_winit::Backend::new().expect("This app is being run on windows"), )) @@ -76,44 +77,76 @@ fn main() -> Result<(), slint::PlatformError> { None } }; - let ini = ini.unwrap_or_else(|| { - warn!("Ini not setup correctly. Creating new Ini"); - new_cfg(current_ini).unwrap_or_else(|err| { - // io::Write error - debug!("error 2"); - errors.push(err); - ini::Ini::new() - }) - }); + let ini = match ini { + Some(ini_data) => Cfg::from(ini_data, current_ini), + None => { + Cfg::read(current_ini).unwrap_or_else(|err| { + // io::write error + debug!("error 2"); + errors.push(err); + Cfg { data: ini::Ini::new(), dir: current_ini.to_owned() } + }) + } + }; let game_verified: bool; + let mod_loader: ModLoader; + let mut mod_loader_cfg: ModLoaderCfg; let mut reg_mods = None; - let game_dir = match attempt_locate_game(current_ini, &ini) { + let order_data: HashMap; + let game_dir = match ini.attempt_locate_game() { Ok(path_result) => match path_result { PathResult::Full(path) => { - match RegMod::collect(current_ini, false) { - Ok(mod_data) => { - game_verified = true; - reg_mods = Some(mod_data); - Some(path) - } - Err(err) => { - // io::Write error | PermissionDenied + mod_loader = ModLoader::properties(&path).unwrap_or_else(|err| { debug!("error 3"); - errors.push(err.clone_err()); - game_verified = true; - Some(path) + errors.push(err); + ModLoader::default() + }); + if mod_loader.installed() { + mod_loader_cfg = ModLoaderCfg::read_section(mod_loader.path(), LOADER_SECTIONS[1]).unwrap_or_else(|err| { + debug!("error 4"); + errors.push(err); + ModLoaderCfg::default() + }); + } else { + mod_loader_cfg = ModLoaderCfg::default(); } - }}, + match mod_loader_cfg.parse_section() { + Ok(data) => order_data = data, + Err(err) => { + debug!("error 5"); + errors.push(err); + order_data = HashMap::new() + } + }; + match ini.collect_mods( Some(&order_data), false) { + Ok(mod_data) => { + reg_mods = Some(mod_data); + } + Err(err) => { + // io::Write error | PermissionDenied + debug!("error 6"); + errors.push(err); + } + }; + game_verified = true; + Some(path) + }, PathResult::Partial(path) | PathResult::None(path) => { + mod_loader_cfg = ModLoaderCfg::default(); + mod_loader = ModLoader::default(); + order_data = HashMap::new(); game_verified = false; Some(path) } }, Err(err) => { // io::Write error - debug!("error 4"); + debug!("error 7"); errors.push(err); + mod_loader_cfg = ModLoaderCfg::default(); + mod_loader = ModLoader::default(); + order_data = HashMap::new(); game_verified = false; None } @@ -127,13 +160,13 @@ fn main() -> Result<(), slint::PlatformError> { Ok(bool) => ui.global::().set_dark_mode(bool.value), Err(err) => { // io::Read error - debug!("error 5"); + debug!("error 8"); errors.push(err); ui.global::().set_dark_mode(true); save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], true) // io::Write error .unwrap_or_else(|err| { - debug!("error 6"); + debug!("error 9"); errors.push(err) }); } @@ -150,24 +183,16 @@ fn main() -> Result<(), slint::PlatformError> { ); let _ = get_or_update_game_dir(Some(game_dir.clone().unwrap_or_default())); - let mod_loader: ModLoader; if !game_verified { ui.global::().set_current_subpage(1); - mod_loader = ModLoader::default(); } else { - let game_dir = game_dir.as_ref().expect("game dir verified"); - mod_loader = ModLoader::properties(game_dir).unwrap_or_else(|err| { - debug!("error 7"); - errors.push(err); - ModLoader::default() - }); deserialize_current_mods( &if let Some(mod_data) = reg_mods { mod_data } else { - RegMod::collect(current_ini, !mod_loader.installed()).unwrap_or_else(|err| { + ini.collect_mods(Some(&order_data),!mod_loader.installed()).unwrap_or_else(|err| { // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error - debug!("error 8"); + debug!("error 10"); errors.push(err); vec![RegMod::default()] }) @@ -177,39 +202,38 @@ fn main() -> Result<(), slint::PlatformError> { if mod_loader.installed() { ui.global::().set_loader_installed(true); - let loader_cfg = ModLoaderCfg::read_section(mod_loader.path(), LOADER_SECTIONS[0]).unwrap(); - let delay = loader_cfg.get_load_delay().unwrap_or_else(|_| { + let delay = mod_loader_cfg.get_load_delay().unwrap_or_else(|_| { // parse error ErrorKind::InvalidData let err = std::io::Error::new(ErrorKind::InvalidData, format!( "Found an unexpected character saved in \"{}\" Reseting to default value", LOADER_KEYS[0] )); error!("{err}"); - debug!("error 9"); + debug!("error 11"); errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_LOADER_VALUES[0]) .unwrap_or_else(|err| { // io::write error error!("{err}"); - debug!("error 10"); + debug!("error 12"); errors.push(err); }); DEFAULT_LOADER_VALUES[0].parse().unwrap() }); - let show_terminal = loader_cfg.get_show_terminal().unwrap_or_else(|_| { + let show_terminal = mod_loader_cfg.get_show_terminal().unwrap_or_else(|_| { // parse error ErrorKind::InvalidData let err = std::io::Error::new(ErrorKind::InvalidData, format!( "Found an unexpected character saved in \"{}\" Reseting to default value", LOADER_KEYS[1] )); error!("{err}"); - debug!("error 11"); + debug!("error 13"); errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_LOADER_VALUES[1]) .unwrap_or_else(|err| { // io::write error error!("{err}"); - debug!("error 12"); + debug!("error 14"); errors.push(err); }); false @@ -242,27 +266,17 @@ fn main() -> Result<(), slint::PlatformError> { } else if game_verified { ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); let _ = receive_msg().await; - match mods_registered(IniOption::Ini(ini)) { - Ok(n) => { - if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), current_ini, n).await { - ui.display_msg(&err.to_string()); - }; - } - Err(err) => ui.display_msg(&err.to_string()) - } + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), &ini).await { + ui.display_msg(&err.to_string()); + }; } } else if game_verified { if !mod_loader.installed() { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", get_or_update_game_dir(None).display())); - } - match mods_registered(IniOption::Ini(ini)) { - Ok(0) => { - if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), current_ini, 0).await { - ui.display_msg(&err.to_string()); - }; + } else if mods_registered(&ini.data) == 0 { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), &ini).await { + ui.display_msg(&err.to_string()); } - Ok(_) => (), - Err(err) => ui.display_msg(&err.to_string()), } } else { ui.display_msg( @@ -278,11 +292,17 @@ fn main() -> Result<(), slint::PlatformError> { ui.global::().on_select_mod_files({ let ui_handle = ui.as_weak(); move |mod_name| { - let current_ini = get_ini_dir(); let ui = ui_handle.unwrap(); + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; let format_key = mod_name.trim().replace(' ', "_"); let mut results: Vec> = Vec::with_capacity(2); - let registered_mods = RegMod::collect(current_ini, false).unwrap_or_else(|err| { + let registered_mods = ini.collect_mods(None, false).unwrap_or_else(|err| { results.push(Err(err)); vec![RegMod::default()] }); @@ -350,7 +370,7 @@ fn main() -> Result<(), slint::PlatformError> { } let state = !files.iter().all(FileData::is_disabled); results.push(save_bool( - current_ini, + &ini.dir, INI_SECTIONS[2], &format_key, state, @@ -358,14 +378,14 @@ fn main() -> Result<(), slint::PlatformError> { match files.len() { 0 => return, 1 => results.push(save_path( - current_ini, + &ini.dir, INI_SECTIONS[3], &format_key, files[0].as_path(), )), 2.. => { let path_refs = files.iter().map(|p| p.as_path()).collect::>(); - results.push(save_paths(current_ini, INI_SECTIONS[3], &format_key, &path_refs)) + results.push(save_paths(&ini.dir, INI_SECTIONS[3], &format_key, &path_refs)) }, } if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { @@ -373,29 +393,29 @@ fn main() -> Result<(), slint::PlatformError> { // If something fails to save attempt to create a corrupt entry so // sync keys will take care of any invalid ini entries let _ = - remove_entry(current_ini, INI_SECTIONS[2], &format_key); + remove_entry(&ini.dir, INI_SECTIONS[2], &format_key); } let new_mod = RegMod::new(&format_key, state, files); new_mod - .verify_state(&game_dir, current_ini) + .verify_state(&game_dir, &ini.dir) .unwrap_or_else(|err| { // Toggle files returned an error lets try it again - if new_mod.verify_state(&game_dir, current_ini).is_err() { + if new_mod.verify_state(&game_dir, &ini.dir).is_err() { ui.display_msg(&err.to_string()); let _ = remove_entry( - current_ini, + &ini.dir, INI_SECTIONS[2], &new_mod.name, ); }; }); - ui.global::() - .set_line_edit_text(SharedString::new()); + ui.global::().set_line_edit_text(SharedString::new()); + let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors - match RegMod::collect(current_ini, false) { + match ini.collect_mods( None, false) { Ok(mods) => mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -411,7 +431,13 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; slint::spawn_local(async move { let game_dir = get_or_update_game_dir(None); let path_result = get_user_folder(&game_dir); @@ -438,8 +464,8 @@ fn main() -> Result<(), slint::PlatformError> { }; match files_not_found(&try_path, &REQUIRED_GAME_FILES) { Ok(not_found) => if not_found.is_empty() { - let result = save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path); - if result.is_err() && save_path(current_ini, INI_SECTIONS[1], INI_KEYS[1], &try_path).is_err() { + let result = save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], &try_path); + if result.is_err() && save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], &try_path).is_err() { let err = result.unwrap_err(); error!("Failed to save directory. {err}"); ui.display_msg(&err.to_string()); @@ -456,14 +482,9 @@ fn main() -> Result<(), slint::PlatformError> { if mod_loader.installed() { ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); let _ = receive_msg().await; - match mods_registered(IniOption::Path(current_ini)) { - Ok(n) => { - if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, current_ini, n).await { - ui.display_msg(&err.to_string()); - }; - } - Err(err) => ui.display_msg(&err.to_string()) - } + if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, &ini).await { + ui.display_msg(&err.to_string()); + }; } else { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") } @@ -488,15 +509,21 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key, state| -> bool { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return !state; + } + }; let game_dir = get_or_update_game_dir(None); let format_key = key.replace(' ', "_"); - match RegMod::collect(current_ini, false) { + match ini.collect_mods(None, false) { Ok(reg_mods) => { if let Some(found_mod) = reg_mods.iter().find(|reg_mod| format_key == reg_mod.name) { - let result = toggle_files(&game_dir, state, found_mod, Some(current_ini)); + let result = toggle_files(&game_dir, state, found_mod, Some(&ini.dir)); if result.is_ok() { return state; } @@ -508,10 +535,11 @@ fn main() -> Result<(), slint::PlatformError> { } Err(err) => ui.display_msg(&err.to_string()), } + let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors - match RegMod::collect(current_ini, false) { + match ini.collect_mods(None, false) { Ok(mods) => mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -534,8 +562,14 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key| { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); - let registered_mods = match RegMod::collect(current_ini, false) { + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; + let registered_mods = match ini.collect_mods(None, false) { Ok(data) => data, Err(err) => { ui.display_msg(&err.to_string()); @@ -600,18 +634,18 @@ fn main() -> Result<(), slint::PlatformError> { let new_data_refs = found_mod.files.add_other_files_to_files(&new_data); if found_mod.files.len() == 1 { results.push(remove_entry( - current_ini, + &ini.dir, INI_SECTIONS[3], &found_mod.name, )); } else { - results.push(remove_array(current_ini, &found_mod.name)); + results.push(remove_array(&ini.dir, &found_mod.name)); } - results.push(save_paths(current_ini, INI_SECTIONS[3], &found_mod.name, &new_data_refs)); + results.push(save_paths(&ini.dir, INI_SECTIONS[3], &found_mod.name, &new_data_refs)); if let Some(err) = results.iter().find_map(|result| result.as_ref().err()) { ui.display_msg(&err.to_string()); let _ = remove_entry( - current_ini, + &ini.dir, INI_SECTIONS[2], &format_key, ); @@ -620,15 +654,15 @@ fn main() -> Result<(), slint::PlatformError> { let updated_mod = RegMod::new(&found_mod.name, found_mod.state, new_data_owned); updated_mod - .verify_state(&game_dir, current_ini) + .verify_state(&game_dir, &ini.dir) .unwrap_or_else(|err| { if updated_mod - .verify_state(&game_dir, current_ini) + .verify_state(&game_dir, &ini.dir) .is_err() { ui.display_msg(&err.to_string()); let _ = remove_entry( - current_ini, + &ini.dir, INI_SECTIONS[2], &updated_mod.name, ); @@ -638,9 +672,10 @@ fn main() -> Result<(), slint::PlatformError> { if !results.iter().any(|r| r.is_err()) { ui.display_msg(&format!("Sucessfully added {} file(s) to {}", num_files, format_key)); } + let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { - match RegMod::collect(current_ini, false) { + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + match ini.collect_mods(None, false) { Ok(mods) => mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -662,14 +697,20 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key| { let ui = ui_handle.unwrap(); - let current_ini = get_ini_dir(); + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; let format_key = key.replace(' ', "_"); ui.display_confirm(&format!("Are you sure you want to de-register: \"{key}\""), false); slint::spawn_local(async move { if receive_msg().await != Message::Confirm { return } - let mut reg_mods = match RegMod::collect(current_ini, false) { + let mut reg_mods = match ini.collect_mods(None, false) { Ok(reg_mods) => reg_mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -682,7 +723,7 @@ fn main() -> Result<(), slint::PlatformError> { reg_mods.iter_mut().find(|reg_mod| format_key == reg_mod.name) { if found_mod.files.dll.iter().any(FileData::is_disabled) { - match toggle_files(&game_dir, true, found_mod, Some(current_ini)) { + match toggle_files(&game_dir, true, found_mod, Some(&ini.dir)) { Ok(files) => { found_mod.files.dll = files; found_mod.state = true; @@ -694,7 +735,7 @@ fn main() -> Result<(), slint::PlatformError> { } } // we can let sync keys take care of removing files from ini - remove_entry(current_ini, INI_SECTIONS[2], &found_mod.name) + remove_entry(&ini.dir, INI_SECTIONS[2], &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); let ui_handle = ui.as_weak(); match confirm_remove_mod(ui_handle, &game_dir, found_mod).await { @@ -713,9 +754,10 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&format!("{err}\nRemoving invalid entries")) }; ui.global::().set_current_subpage(0); + let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( - &RegMod::collect(current_ini, false).unwrap_or_else(|_| { - match RegMod::collect(current_ini, false) { + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + match ini.collect_mods(None, false) { Ok(mods) => mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -771,15 +813,9 @@ fn main() -> Result<(), slint::PlatformError> { move |state| -> bool { let ui = ui_handle.unwrap(); let value = if state { "1" } else { "0" }; - let ext_ini = match ModLoader::properties(&get_or_update_game_dir(None)) { - Ok(ini) => ini.own_path(), - Err(err) => { - ui.display_msg(&err.to_string()); - return !state - } - }; + let ext_ini = get_loader_ini_dir(); let mut result = state; - save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( + save_value_ext(ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[1], value).unwrap_or_else( |err| { ui.display_msg(&err.to_string()); result = !state; @@ -792,15 +828,9 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |time| { let ui = ui_handle.unwrap(); - let ext_ini = match ModLoader::properties(&get_or_update_game_dir(None)) { - Ok(ini) => ini.own_path(), - Err(err) => { - ui.display_msg(&err.to_string()); - return - } - }; + let ext_ini = get_loader_ini_dir(); ui.global::().invoke_force_app_focus(); - if let Err(err) = save_value_ext(&ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { + if let Err(err) = save_value_ext(ext_ini, LOADER_SECTIONS[0], LOADER_KEYS[0], &time) { ui.display_msg(&format!("Failed to set load delay\n\n{err}")); return; } @@ -866,16 +896,17 @@ fn main() -> Result<(), slint::PlatformError> { move || { let ui = ui_handle.unwrap(); slint::spawn_local(async move { - let current_ini = get_ini_dir(); let game_dir = get_or_update_game_dir(None); - match mods_registered(IniOption::Path(current_ini)) { - Ok(n) => { - if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir, current_ini, n).await { - ui.display_msg(&err.to_string()); - }; + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; } - Err(err) => ui.display_msg(&err.to_string()) - } + }; + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir, &ini).await { + ui.display_msg(&err.to_string()); + }; }).unwrap(); } }); @@ -884,9 +915,9 @@ fn main() -> Result<(), slint::PlatformError> { move |state, key, value| -> i32 { let ui = ui_handle.unwrap(); let error = 42069_i32; - let cfg_dir = get_or_update_game_dir(None).join(LOADER_FILES[2]); + let cfg_dir = get_loader_ini_dir(); let mut result: i32 = if state { 1 } else { -1 }; - let mut load_order = match ModLoaderCfg::read_section(&cfg_dir, LOADER_SECTIONS[1]) { + let mut load_order = match ModLoaderCfg::read_section(cfg_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err.to_string()); @@ -919,8 +950,8 @@ fn main() -> Result<(), slint::PlatformError> { move |to_k, from_k, value| -> i32 { let ui = ui_handle.unwrap(); let mut result = 0_i32; - let cfg_dir = get_or_update_game_dir(None).join(LOADER_FILES[2]); - let mut load_order = match ModLoaderCfg::read_section(&cfg_dir, LOADER_SECTIONS[1]) { + let cfg_dir = get_loader_ini_dir(); + let mut load_order = match ModLoaderCfg::read_section(cfg_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { ui.display_msg(&err.to_string()); @@ -949,7 +980,15 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move || { let ui = ui_handle.unwrap(); - deserialize_current_mods(&RegMod::collect(get_ini_dir(), false).unwrap_or_else(|err| { + let ini = match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return; + } + }; + let order_data = order_data_or_default(ui.as_weak(), None); + deserialize_current_mods(&ini.collect_mods(Some(&order_data), false).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }), ui.as_weak()); @@ -1032,6 +1071,13 @@ fn get_ini_dir() -> &'static PathBuf { }) } +fn get_loader_ini_dir() -> &'static PathBuf { + static LOADER_CONFIG_PATH: OnceLock = OnceLock::new(); + LOADER_CONFIG_PATH.get_or_init(|| { + get_or_update_game_dir(None).join(LOADER_FILES[2]) + }) +} + fn get_or_update_game_dir(update: Option) -> tokio::sync::RwLockReadGuard<'static, std::path::PathBuf> { static GAME_DIR: OnceLock> = OnceLock::new(); @@ -1085,6 +1131,26 @@ fn open_text_files(ui_handle: slint::Weak, files: Vec) } } +fn order_data_or_default(ui_handle: slint::Weak, from_path: Option<&Path>) -> HashMap { + let ui = ui_handle.unwrap(); + let path: &Path; + if let Some(dir) = from_path { + path = dir + } else { path = get_loader_ini_dir() }; + match ModLoaderCfg::read_section(path, LOADER_SECTIONS[1]) { + Ok(mut data) => { + data.parse_section().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + HashMap::new() + }) + }, + Err(err) => { + ui.display_msg(&err.to_string()); + HashMap::new() + } + } +} + fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { let ui = ui_handle.unwrap(); let display_mods: Rc> = Default::default(); @@ -1257,31 +1323,19 @@ async fn confirm_remove_mod( remove_mod_files(game_dir, reg_mod) } -enum IniOption<'a> { - Ini(ini::Ini), - Path(&'a Path), -} - -fn mods_registered(input_ini: IniOption) -> std::io::Result { - let ini = match input_ini { - IniOption::Ini(ini) => ini, - IniOption::Path(ini_file) => { - match get_or_setup_cfg(ini_file, &INI_SECTIONS) { - Ok(ini) => ini, - Err(err) => return Err(err), - } - } - }; +/// returns the number of registered mods currently saved in the ".ini" +/// this can't error if you pass in a `IniOption::Ini` +fn mods_registered(ini: &ini::Ini) -> usize { let empty_ini = ini.section(INI_SECTIONS[2]).is_none() || ini.section(INI_SECTIONS[2]).unwrap().is_empty(); - if empty_ini { Ok(0) } else { Ok(ini.section(INI_SECTIONS[2]).unwrap().len()) } + if empty_ini { 0 } else { ini.section(INI_SECTIONS[2]).unwrap().len() } } async fn confirm_scan_mods( ui_weak: slint::Weak, game_dir: &Path, - ini_file: &Path, - mods_registered: usize) -> std::io::Result<()>{ + ini: &Cfg) -> std::io::Result<()> { let ui = ui_weak.unwrap(); + let mods_registered = mods_registered(&ini.data); ui.display_confirm("Would you like to attempt to auto-import already installed mods to Elden Mod Loader GUI?", true); if receive_msg().await != Message::Confirm { @@ -1296,26 +1350,32 @@ async fn confirm_scan_mods( let dark_mode = ui.global::().get_dark_mode(); // MARK: TODO // need to check if a deleted mod was in the disabled state and then toggle if so - std::fs::remove_file(ini_file)?; - new_cfg(ini_file)?; - save_bool(ini_file, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; - save_path(ini_file, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; + std::fs::remove_file(&ini.dir)?; + new_cfg(&ini.dir)?; + save_bool(&ini.dir, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; + save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; } - match scan_for_mods(game_dir, ini_file) { + match scan_for_mods(game_dir, &ini.dir) { Ok(len) => { + let ini = match Cfg::read(&ini.dir) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return Err(err); + } + }; ui.global::().set_current_subpage(0); let mod_loader = ModLoader::properties(game_dir).unwrap_or_default(); + let order_data = order_data_or_default(ui.as_weak(), Some(mod_loader.path())); deserialize_current_mods( - &RegMod::collect(ini_file, !mod_loader.installed()).unwrap_or_else(|err| { + &ini.collect_mods(Some(&order_data), !mod_loader.installed()).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }),ui.as_weak() ); ui.display_msg(&format!("Successfully Found {len} mod(s)")); } - Err(err) => if err.kind() != ErrorKind::ConnectionAborted { - ui.display_msg(&format!("Error: {err}")); - } + Err(err) => ui.display_msg(&format!("Error: {err}")), }; Ok(()) } \ No newline at end of file diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index f492d4c..2f77579 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -24,37 +24,34 @@ pub struct ModLoader { impl ModLoader { pub fn properties(game_dir: &Path) -> std::io::Result { - let cfg_dir = game_dir.join(LOADER_FILES[2]); + let mut cfg_dir = game_dir.join(LOADER_FILES[2]); + let mut properties = ModLoader::default(); match does_dir_contain(game_dir, Operation::Count, &LOADER_FILES) { Ok(OperationResult::Count((_, files))) => { if files.contains(LOADER_FILES[1]) && !files.contains(LOADER_FILES[0]) { trace!("Mod loader found in the Enabled state"); - if !files.contains(LOADER_FILES[2]) { - new_cfg(&cfg_dir)?; - } - Ok(ModLoader { - installed: true, - disabled: false, - path: cfg_dir, - }) + properties.installed = true; } else if files.contains(LOADER_FILES[0]) && !files.contains(LOADER_FILES[1]) { trace!("Mod loader found in the Disabled state"); - if !files.contains(LOADER_FILES[2]) { - new_cfg(&cfg_dir)?; - } - Ok(ModLoader { - installed: true, - disabled: true, - path: cfg_dir, - }) - } else { - warn!("Mod loader dll hook not found"); - Ok(ModLoader::default()) + properties.installed = true; + properties.disabled = true; + } + if files.contains(LOADER_FILES[2]) { + std::mem::swap(&mut cfg_dir, &mut properties.path); } } - Err(err) => Err(err), + Err(err) => return Err(err), _ => unreachable!(), + }; + if properties.installed && properties.path == Path::new("") { + trace!("{} not found, creating new", LOADER_FILES[2]); + new_cfg(&cfg_dir)?; + properties.path = cfg_dir; + } + if !properties.installed { + warn!("Mod loader dll hook not found"); } + Ok(properties) } #[inline] @@ -80,8 +77,8 @@ impl ModLoader { #[derive(Debug, Default)] pub struct ModLoaderCfg { - cfg: Ini, - cfg_dir: PathBuf, + data: Ini, + dir: PathBuf, section: Option, } @@ -91,16 +88,16 @@ impl ModLoaderCfg { return new_io_error!(ErrorKind::InvalidInput, "section can not be none"); } - let cfg = get_or_setup_cfg(cfg_dir, &LOADER_SECTIONS)?; + let data = get_or_setup_cfg(cfg_dir, &LOADER_SECTIONS)?; Ok(ModLoaderCfg { - cfg, - cfg_dir: PathBuf::from(cfg_dir), + data, + dir: PathBuf::from(cfg_dir), section: section.map(String::from), }) } pub fn get_load_delay(&self) -> std::io::Result { - match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[0]) { + match IniProperty::::read(&self.data, LOADER_SECTIONS[0], LOADER_KEYS[0]) { Ok(delay_time) => Ok(delay_time.value), Err(err) => Err(err.add_msg(format!( "Found an unexpected character saved in \"{}\"", @@ -110,7 +107,7 @@ impl ModLoaderCfg { } pub fn get_show_terminal(&self) -> std::io::Result { - match IniProperty::::read(&self.cfg, LOADER_SECTIONS[0], LOADER_KEYS[1]) { + match IniProperty::::read(&self.data, LOADER_SECTIONS[0], LOADER_KEYS[1]) { Ok(delay_time) => Ok(delay_time.value), Err(err) => Err(err.add_msg(format!( "Found an unexpected character saved in \"{}\"", @@ -121,12 +118,12 @@ impl ModLoaderCfg { #[inline] pub fn mut_section(&mut self) -> &mut ini::Properties { - self.cfg.section_mut(self.section.as_ref()).unwrap() + self.data.section_mut(self.section.as_ref()).unwrap() } #[inline] fn section(&self) -> &ini::Properties { - self.cfg.section(self.section.as_ref()).unwrap() + self.data.section(self.section.as_ref()).unwrap() } #[inline] @@ -179,11 +176,11 @@ impl ModLoaderCfg { #[inline] pub fn path(&self) -> &Path { - &self.cfg_dir + &self.dir } pub fn write_to_file(&self) -> std::io::Result<()> { - self.cfg.write_to_file_opt(&self.cfg_dir, EXT_OPTIONS) + self.data.write_to_file_opt(&self.dir, EXT_OPTIONS) } /// updates the load order values in `Some("loadorder")` so they are always `0..` diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 896a04c..ffc43b8 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -7,13 +7,9 @@ use std::{ }; use crate::{ - files_not_found, get_cfg, new_io_error, toggle_files, - utils::ini::{ - mod_loader::ModLoaderCfg, - writer::{remove_array, remove_entry}, - }, - FileData, ARRAY_KEY, ARRAY_VALUE, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, - OFF_STATE, REQUIRED_GAME_FILES, + files_not_found, new_io_error, toggle_files, + utils::ini::writer::{remove_array, remove_entry}, + Cfg, FileData, ARRAY_KEY, ARRAY_VALUE, INI_KEYS, INI_SECTIONS, OFF_STATE, REQUIRED_GAME_FILES, }; pub trait Parsable: Sized { @@ -480,6 +476,21 @@ impl RegMod { } } + pub fn verify_state(&self, game_dir: &Path, ini_path: &Path) -> std::io::Result<()> { + if (!self.state && self.files.dll.iter().any(FileData::is_enabled)) + || (self.state && self.files.dll.iter().any(FileData::is_disabled)) + { + warn!( + "wrong file state for \"{}\" chaning file extentions", + self.name + ); + let _ = toggle_files(game_dir, self.state, self, Some(ini_path))?; + } + Ok(()) + } +} + +impl Cfg { // MARK: FIXME // when is the best time to verify parsed data? currently we verify data after shaping it // the code would most likely be cleaner if we verified it apon parsing before doing any shaping @@ -487,7 +498,11 @@ impl RegMod { // should we have two collections? one for deserialization(full) one for just collect and verify // collect needs to be completely recoverable, runing into an error and then returning a default is not good enough - pub fn collect(ini_path: &Path, skip_validation: bool) -> std::io::Result> { + pub fn collect_mods( + &self, + include_load_order: Option<&HashMap>, + skip_validation: bool, + ) -> std::io::Result> { type CollectedMaps<'a> = (HashMap<&'a str, &'a str>, HashMap<&'a str, Vec<&'a str>>); type ModData<'a> = Vec<( &'a str, @@ -556,7 +571,7 @@ impl RegMod { fn combine_map_data<'a>( map_data: CollectedMaps<'a>, - parsed_order_val: &HashMap, + parsed_order_val: Option<&HashMap>, ) -> ModData<'a> { let mut count = 0_usize; let mut mod_data = map_data @@ -567,7 +582,10 @@ impl RegMod { let split_files = SplitFiles::from( file_strs.iter().map(PathBuf::from).collect::>(), ); - let load_order = LoadOrder::from(&split_files.dll, parsed_order_val); + let load_order = match parsed_order_val { + Some(data) => LoadOrder::from(&split_files.dll, data), + None => LoadOrder::default(), + }; if load_order.set { count += 1 } @@ -613,10 +631,8 @@ impl RegMod { .collect() } - let ini = get_cfg(ini_path)?; - if skip_validation { - let parsed_data = collect_data_unchecked(&ini); + let parsed_data = collect_data_unchecked(&self.data); Ok(parsed_data .iter() .map(|(n, s, f)| { @@ -628,52 +644,37 @@ impl RegMod { }) .collect()) } else { - let parsed_data = sync_keys(&ini, ini_path)?; + let parsed_data = sync_keys(&self.data, &self.dir)?; let game_dir = - IniProperty::::read(&ini, INI_SECTIONS[1], INI_KEYS[1], false)?.value; + IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false)? + .value; // parse_section is non critical write error | read_section is also non critical write error - let load_order_parsed = - ModLoaderCfg::read_section(&game_dir.join(LOADER_FILES[2]), LOADER_SECTIONS[1])? - .parse_section()?; - let parsed_data = combine_map_data(parsed_data, &load_order_parsed); + let parsed_data = combine_map_data(parsed_data, include_load_order); let mut output = Vec::with_capacity(parsed_data.len()); for (k, s, f, l) in parsed_data { match &s { Ok(bool) => { if let Err(err) = f.file_refs().validate(Some(&game_dir)) { error!("Error: {err}"); - remove_entry(ini_path, INI_SECTIONS[2], k).expect("Key is valid"); + remove_entry(&self.dir, INI_SECTIONS[2], k).expect("Key is valid"); } else { let reg_mod = RegMod::from_split_files(k, *bool, f, l); // MARK: FIXME // verify_state should be ran within collect, but this call is too late, we should handle verification earilier // when sync keys hits an error we should give it a chance to correct by calling verify_state before it deletes an entry - reg_mod.verify_state(&game_dir, ini_path)?; + reg_mod.verify_state(&game_dir, &self.dir)?; output.push(reg_mod) } } Err(err) => { error!("Error: {err}"); - remove_entry(ini_path, INI_SECTIONS[2], k).expect("Key is valid"); + remove_entry(&self.dir, INI_SECTIONS[2], k).expect("Key is valid"); } } } Ok(output) } } - - pub fn verify_state(&self, game_dir: &Path, ini_path: &Path) -> std::io::Result<()> { - if (!self.state && self.files.dll.iter().any(FileData::is_enabled)) - || (self.state && self.files.dll.iter().any(FileData::is_disabled)) - { - warn!( - "wrong file state for \"{}\" chaning file extentions", - self.name - ); - let _ = toggle_files(game_dir, self.state, self, Some(ini_path))?; - } - Ok(()) - } } pub fn file_registered(mod_data: &[RegMod], files: &[PathBuf]) -> bool { diff --git a/tests/test_ini_tools.rs b/tests/test_ini_tools.rs index 5db5a9f..c3b8471 100644 --- a/tests/test_ini_tools.rs +++ b/tests/test_ini_tools.rs @@ -14,7 +14,7 @@ mod tests { parser::{IniProperty, RegMod, Setup}, writer::*, }, - INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, OFF_STATE, + Cfg, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_SECTIONS, OFF_STATE, }; use crate::common::{new_cfg_with_sections, GAME_DIR}; @@ -293,7 +293,8 @@ mod tests { // -------------------------------------sync_keys() runs from inside RegMod::collect()------------------------------------------------ // ----this deletes any keys that do not have a matching state eg. (key has state but no files, or key has files but no state)----- // this tests delete_entry && delete_array in this case we delete "no_matching_path", "no_matching_state_1", and "no_matching_state_2" - let registered_mods = RegMod::collect(test_file, false).unwrap(); + let cfg = Cfg::read(test_file).unwrap(); + let registered_mods = cfg.collect_mods(None, false).unwrap(); assert_eq!(registered_mods.len(), 2); // verify_state() also runs from within RegMod::collect() lets see if changed the state of the mods .dll file From d2d4658a362303e7817d7660e30839cb184f3024 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 5 May 2024 15:10:08 -0500 Subject: [PATCH 53/62] Bug fixes had to update to take in to account the removal of in memory data --- src/lib.rs | 5 ++++- src/main.rs | 9 ++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 38c5684..104d9c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -377,7 +377,7 @@ impl Cfg { }) } - pub fn attempt_locate_game(&self) -> std::io::Result { + pub fn attempt_locate_game(&mut self) -> std::io::Result { if let Ok(path) = IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false) { @@ -396,6 +396,9 @@ impl Cfg { INI_KEYS[1], try_locate.as_path(), )?; + self.data + .with_section(INI_SECTIONS[1]) + .set(INI_KEYS[1], try_locate.to_string_lossy().to_string()); return Ok(PathResult::Full(try_locate)); } if try_locate.components().count() > 1 { diff --git a/src/main.rs b/src/main.rs index 4889941..b61a280 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,7 +77,7 @@ fn main() -> Result<(), slint::PlatformError> { None } }; - let ini = match ini { + let mut ini = match ini { Some(ini_data) => Cfg::from(ini_data, current_ini), None => { Cfg::read(current_ini).unwrap_or_else(|err| { @@ -129,6 +129,13 @@ fn main() -> Result<(), slint::PlatformError> { errors.push(err); } }; + if reg_mods.is_some() && reg_mods.as_ref().unwrap().len() != mods_registered(&ini.data) { + ini = Cfg::read(current_ini).unwrap_or_else(|err| { + debug!("error 7"); + errors.push(err); + Cfg { data: ini::Ini::new(), dir: current_ini.to_owned() } + }) + } game_verified = true; Some(path) }, From 9de6e66b802039d7643d7dd46a7f49812fef89aa Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 5 May 2024 16:59:34 -0500 Subject: [PATCH 54/62] update read data bugs more bugs with old data being set to the front end because of previous change to how collect_mods works --- src/lib.rs | 5 --- src/main.rs | 89 +++++++++++++++++++++++++---------------- src/utils/ini/parser.rs | 18 +-------- 3 files changed, 55 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 104d9c8..72c8378 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,11 +66,6 @@ macro_rules! new_io_error { }; } -pub enum IniOption<'a> { - Ini(&'a ini::Ini), - Path(&'a Path), -} - pub struct PathErrors { pub ok_paths_short: Vec, pub err_paths_long: Vec, diff --git a/src/main.rs b/src/main.rs index b61a280..513ea04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -149,7 +149,7 @@ fn main() -> Result<(), slint::PlatformError> { }, Err(err) => { // io::Write error - debug!("error 7"); + debug!("error 8"); errors.push(err); mod_loader_cfg = ModLoaderCfg::default(); mod_loader = ModLoader::default(); @@ -167,13 +167,13 @@ fn main() -> Result<(), slint::PlatformError> { Ok(bool) => ui.global::().set_dark_mode(bool.value), Err(err) => { // io::Read error - debug!("error 8"); + debug!("error 9"); errors.push(err); ui.global::().set_dark_mode(true); save_bool(current_ini, INI_SECTIONS[0], INI_KEYS[0], true) // io::Write error .unwrap_or_else(|err| { - debug!("error 9"); + debug!("error 10"); errors.push(err) }); } @@ -199,7 +199,7 @@ fn main() -> Result<(), slint::PlatformError> { } else { ini.collect_mods(Some(&order_data),!mod_loader.installed()).unwrap_or_else(|err| { // io::Error from toggle files | ErrorKind::InvalidInput - did not pass len check | io::Write error - debug!("error 10"); + debug!("error 11"); errors.push(err); vec![RegMod::default()] }) @@ -216,13 +216,13 @@ fn main() -> Result<(), slint::PlatformError> { LOADER_KEYS[0] )); error!("{err}"); - debug!("error 11"); + debug!("error 12"); errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[0], DEFAULT_LOADER_VALUES[0]) .unwrap_or_else(|err| { // io::write error error!("{err}"); - debug!("error 12"); + debug!("error 13"); errors.push(err); }); DEFAULT_LOADER_VALUES[0].parse().unwrap() @@ -234,13 +234,13 @@ fn main() -> Result<(), slint::PlatformError> { LOADER_KEYS[1] )); error!("{err}"); - debug!("error 13"); + debug!("error 14"); errors.push(err); save_value_ext(mod_loader.path(), LOADER_SECTIONS[0], LOADER_KEYS[1], DEFAULT_LOADER_VALUES[1]) .unwrap_or_else(|err| { // io::write error error!("{err}"); - debug!("error 14"); + debug!("error 15"); errors.push(err); }); false @@ -273,7 +273,7 @@ fn main() -> Result<(), slint::PlatformError> { } else if game_verified { ui.display_msg("Welcome to Elden Mod Loader GUI!\nThanks for downloading, please report any bugs\n\nGame Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); let _ = receive_msg().await; - if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), &ini).await { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), Some(ini)).await { ui.display_msg(&err.to_string()); }; } @@ -281,7 +281,7 @@ fn main() -> Result<(), slint::PlatformError> { if !mod_loader.installed() { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", get_or_update_game_dir(None).display())); } else if mods_registered(&ini.data) == 0 { - if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), &ini).await { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), Some(ini)).await { ui.display_msg(&err.to_string()); } } @@ -300,7 +300,8 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |mod_name| { let ui = ui_handle.unwrap(); - let ini = match Cfg::read(get_ini_dir()) { + let ini_dir = get_ini_dir(); + let ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -418,17 +419,16 @@ fn main() -> Result<(), slint::PlatformError> { }; }); ui.global::().set_line_edit_text(SharedString::new()); + drop(ini); let order_data = order_data_or_default(ui.as_weak(), None); + let ini = Cfg::read(ini_dir).unwrap_or_default(); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors - match ini.collect_mods( None, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } + }) }),ui.as_weak() ); }).unwrap(); @@ -489,7 +489,7 @@ fn main() -> Result<(), slint::PlatformError> { if mod_loader.installed() { ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); let _ = receive_msg().await; - if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, &ini).await { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, Some(ini)).await { ui.display_msg(&err.to_string()); }; } else { @@ -516,7 +516,8 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key, state| -> bool { let ui = ui_handle.unwrap(); - let ini = match Cfg::read(get_ini_dir()) { + let ini_dir = get_ini_dir(); + let ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -542,6 +543,11 @@ fn main() -> Result<(), slint::PlatformError> { } Err(err) => ui.display_msg(&err.to_string()), } + drop(ini); + let ini = Cfg::read(ini_dir).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + Cfg::default() + }); let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { @@ -569,7 +575,8 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key| { let ui = ui_handle.unwrap(); - let ini = match Cfg::read(get_ini_dir()) { + let ini_dir = get_ini_dir(); + let ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -679,6 +686,11 @@ fn main() -> Result<(), slint::PlatformError> { if !results.iter().any(|r| r.is_err()) { ui.display_msg(&format!("Sucessfully added {} file(s) to {}", num_files, format_key)); } + drop(ini); + let ini = Cfg::read(ini_dir).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + Cfg::default() + }); let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { @@ -704,7 +716,8 @@ fn main() -> Result<(), slint::PlatformError> { let ui_handle = ui.as_weak(); move |key| { let ui = ui_handle.unwrap(); - let ini = match Cfg::read(get_ini_dir()) { + let ini_dir = get_ini_dir(); + let ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -723,14 +736,14 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&err.to_string()); return; } - }; + drop(ini); let game_dir = get_or_update_game_dir(None); if let Some(found_mod) = reg_mods.iter_mut().find(|reg_mod| format_key == reg_mod.name) { if found_mod.files.dll.iter().any(FileData::is_disabled) { - match toggle_files(&game_dir, true, found_mod, Some(&ini.dir)) { + match toggle_files(&game_dir, true, found_mod, Some(ini_dir)) { Ok(files) => { found_mod.files.dll = files; found_mod.state = true; @@ -742,7 +755,7 @@ fn main() -> Result<(), slint::PlatformError> { } } // we can let sync keys take care of removing files from ini - remove_entry(&ini.dir, INI_SECTIONS[2], &found_mod.name) + remove_entry(ini_dir, INI_SECTIONS[2], &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); let ui_handle = ui.as_weak(); match confirm_remove_mod(ui_handle, &game_dir, found_mod).await { @@ -762,6 +775,7 @@ fn main() -> Result<(), slint::PlatformError> { }; ui.global::().set_current_subpage(0); let order_data = order_data_or_default(ui.as_weak(), None); + let ini = Cfg::read(ini_dir).unwrap_or_default(); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { match ini.collect_mods(None, false) { @@ -904,14 +918,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); slint::spawn_local(async move { let game_dir = get_or_update_game_dir(None); - let ini = match Cfg::read(get_ini_dir()) { - Ok(ini_data) => ini_data, - Err(err) => { - ui.display_msg(&err.to_string()); - return; - } - }; - if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir, &ini).await { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir, None).await { ui.display_msg(&err.to_string()); }; }).unwrap(); @@ -1340,14 +1347,26 @@ fn mods_registered(ini: &ini::Ini) -> usize { async fn confirm_scan_mods( ui_weak: slint::Weak, game_dir: &Path, - ini: &Cfg) -> std::io::Result<()> { + ini: Option) -> std::io::Result<()> { let ui = ui_weak.unwrap(); - let mods_registered = mods_registered(&ini.data); - + ui.display_confirm("Would you like to attempt to auto-import already installed mods to Elden Mod Loader GUI?", true); if receive_msg().await != Message::Confirm { return Ok(()); }; + + let ini = match ini { + Some(data) => data, + None => match Cfg::read(get_ini_dir()) { + Ok(ini_data) => ini_data, + Err(err) => { + ui.display_msg(&err.to_string()); + return Err(err); + } + } + }; + + let mods_registered = mods_registered(&ini.data); let empty_ini = mods_registered == 0; if !empty_ini { ui.display_confirm("Warning: This action will reset current registered mods, are you sure you want to continue?", true); diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index ffc43b8..d37614e 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -1,5 +1,5 @@ use ini::{Ini, Properties}; -use log::{error, trace, warn}; +use log::{error, warn}; use std::{ collections::HashMap, io::ErrorKind, @@ -206,28 +206,12 @@ fn validate_existance(path: &Path) -> std::io::Result<()> { pub trait Setup { fn is_setup(&self, sections: &[Option<&str>]) -> bool; - fn init_section(&mut self, section: Option<&str>) -> std::io::Result<()>; } impl Setup for Ini { fn is_setup(&self, sections: &[Option<&str>]) -> bool { sections.iter().all(|§ion| self.section(section).is_some()) } - - fn init_section(&mut self, section: Option<&str>) -> std::io::Result<()> { - trace!( - "Section: \"{}\" not found creating new", - section.expect("Passed in section not valid") - ); - self.with_section(section).set("setter_temp_val", "0"); - if self.delete_from(section, "setter_temp_val").is_none() { - return new_io_error!( - ErrorKind::BrokenPipe, - format!("Failed to create a new section: \"{}\"", section.unwrap()) - ); - }; - Ok(()) - } } #[derive(Debug)] From 1c7864b6227e7a423a69ceaa3c487b604c2063be Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Sun, 5 May 2024 19:38:45 -0500 Subject: [PATCH 55/62] disabled check for scan mods for any reason if a mod was not able to be manually scanned in, and the mod was in the disabled state it will now be put back into its enabled state --- src/lib.rs | 6 ++++- src/main.rs | 59 ++++++++++++++++++++++++++--------------- src/utils/ini/parser.rs | 4 +++ 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 72c8378..78c1403 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -350,7 +350,6 @@ pub struct Cfg { pub data: Ini, pub dir: PathBuf, } - pub enum PathResult { Full(PathBuf), Partial(PathBuf), @@ -372,6 +371,11 @@ impl Cfg { }) } + pub fn update(&mut self) -> std::io::Result<()> { + self.data = get_or_setup_cfg(&self.dir, &INI_SECTIONS)?; + Ok(()) + } + pub fn attempt_locate_game(&mut self) -> std::io::Result { if let Ok(path) = IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false) diff --git a/src/main.rs b/src/main.rs index 513ea04..6a76a96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -301,7 +301,7 @@ fn main() -> Result<(), slint::PlatformError> { move |mod_name| { let ui = ui_handle.unwrap(); let ini_dir = get_ini_dir(); - let ini = match Cfg::read(ini_dir) { + let mut ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -419,9 +419,11 @@ fn main() -> Result<(), slint::PlatformError> { }; }); ui.global::().set_line_edit_text(SharedString::new()); - drop(ini); + ini.update().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + ini = Cfg::default(); + }); let order_data = order_data_or_default(ui.as_weak(), None); - let ini = Cfg::read(ini_dir).unwrap_or_default(); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { // if error lets try it again and see if we can get sync-keys to cleanup any errors @@ -517,7 +519,7 @@ fn main() -> Result<(), slint::PlatformError> { move |key, state| -> bool { let ui = ui_handle.unwrap(); let ini_dir = get_ini_dir(); - let ini = match Cfg::read(ini_dir) { + let mut ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -543,10 +545,9 @@ fn main() -> Result<(), slint::PlatformError> { } Err(err) => ui.display_msg(&err.to_string()), } - drop(ini); - let ini = Cfg::read(ini_dir).unwrap_or_else(|err| { + ini.update().unwrap_or_else(|err| { ui.display_msg(&err.to_string()); - Cfg::default() + ini = Cfg::default(); }); let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( @@ -576,7 +577,7 @@ fn main() -> Result<(), slint::PlatformError> { move |key| { let ui = ui_handle.unwrap(); let ini_dir = get_ini_dir(); - let ini = match Cfg::read(ini_dir) { + let mut ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -686,10 +687,9 @@ fn main() -> Result<(), slint::PlatformError> { if !results.iter().any(|r| r.is_err()) { ui.display_msg(&format!("Sucessfully added {} file(s) to {}", num_files, format_key)); } - drop(ini); - let ini = Cfg::read(ini_dir).unwrap_or_else(|err| { + ini.update().unwrap_or_else(|err| { ui.display_msg(&err.to_string()); - Cfg::default() + ini = Cfg::default(); }); let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( @@ -717,7 +717,7 @@ fn main() -> Result<(), slint::PlatformError> { move |key| { let ui = ui_handle.unwrap(); let ini_dir = get_ini_dir(); - let ini = match Cfg::read(ini_dir) { + let mut ini = match Cfg::read(ini_dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -737,7 +737,6 @@ fn main() -> Result<(), slint::PlatformError> { return; } }; - drop(ini); let game_dir = get_or_update_game_dir(None); if let Some(found_mod) = reg_mods.iter_mut().find(|reg_mod| format_key == reg_mod.name) @@ -774,8 +773,11 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&format!("{err}\nRemoving invalid entries")) }; ui.global::().set_current_subpage(0); + ini.update().unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + ini = Cfg::default(); + }); let order_data = order_data_or_default(ui.as_weak(), None); - let ini = Cfg::read(ini_dir).unwrap_or_default(); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { match ini.collect_mods(None, false) { @@ -1366,24 +1368,24 @@ async fn confirm_scan_mods( } }; - let mods_registered = mods_registered(&ini.data); - let empty_ini = mods_registered == 0; + let num_registered = mods_registered(&ini.data); + let empty_ini = num_registered == 0; if !empty_ini { ui.display_confirm("Warning: This action will reset current registered mods, are you sure you want to continue?", true); if receive_msg().await != Message::Confirm { return Ok(()); }; let dark_mode = ui.global::().get_dark_mode(); - // MARK: TODO - // need to check if a deleted mod was in the disabled state and then toggle if so + std::fs::remove_file(&ini.dir)?; new_cfg(&ini.dir)?; save_bool(&ini.dir, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; } + let new_ini: Cfg; match scan_for_mods(game_dir, &ini.dir) { Ok(len) => { - let ini = match Cfg::read(&ini.dir) { + new_ini = match Cfg::read(&ini.dir) { Ok(ini_data) => ini_data, Err(err) => { ui.display_msg(&err.to_string()); @@ -1394,14 +1396,29 @@ async fn confirm_scan_mods( let mod_loader = ModLoader::properties(game_dir).unwrap_or_default(); let order_data = order_data_or_default(ui.as_weak(), Some(mod_loader.path())); deserialize_current_mods( - &ini.collect_mods(Some(&order_data), !mod_loader.installed()).unwrap_or_else(|err| { + &new_ini.collect_mods(Some(&order_data), !mod_loader.installed()).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }),ui.as_weak() ); ui.display_msg(&format!("Successfully Found {len} mod(s)")); } - Err(err) => ui.display_msg(&format!("Error: {err}")), + Err(err) => { + ui.display_msg(&format!("Error: {err}")); + new_ini = Cfg::default(); + }, }; + if new_ini.dir != Path::new("") && num_registered != mods_registered(&new_ini.data) { + let mut old_mods = ini.collect_mods(None, false)?; + old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); + if old_mods.is_empty() { return Ok(()) } + + let new_mods = new_ini.collect_mods(None, false)?; + let all_new_dlls = new_mods.iter().flat_map(|m| m.files.dll_refs()).collect::>(); + old_mods.retain(|m| !m.files.dll.iter().any(|f| all_new_dlls.contains(&f.as_path()))); + if old_mods.is_empty() { return Ok(()) } + + old_mods.iter().try_for_each(|m| toggle_files(game_dir, true, m, None).map(|_| ()))?; + } Ok(()) } \ No newline at end of file diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index d37614e..0f1362e 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -394,6 +394,10 @@ impl SplitFiles { path_refs } + pub fn dll_refs(&self) -> Vec<&Path> { + self.dll.iter().map(|f| f.as_path()).collect() + } + /// returns references to `input_files` + `self.config` + `self.other` pub fn add_other_files_to_files<'a>(&'a self, files: &'a [PathBuf]) -> Vec<&'a Path> { let mut path_refs = Vec::with_capacity(files.len() + self.other_files_len()); From 177ac72a8163005212fa49f04ef92418cd3afd61 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 7 May 2024 17:44:56 -0500 Subject: [PATCH 56/62] Cargo update --- Cargo.lock | 120 ++++++++++++++++++++++++++--------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5068da8..ffbf628 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,7 +454,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -489,7 +489,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -555,7 +555,7 @@ dependencies = [ "derive_utils", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -604,7 +604,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.60", + "syn 2.0.61", "which", ] @@ -713,7 +713,7 @@ checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -762,9 +762,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.96" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065a29261d53ba54260972629f9ca6bffa69bac13cd1fed61420f7fa68b9f8bd" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" dependencies = [ "jobserver", "libc", @@ -987,7 +987,7 @@ checksum = "5387f5bbc9e9e6c96436ea125afa12614cebf8ac67f49abc08c1e7a891466c90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1108,7 +1108,7 @@ dependencies = [ "lazy_static", "proc-macro2", "regex", - "syn 2.0.60", + "syn 2.0.61", "unicode-xid", ] @@ -1120,7 +1120,7 @@ checksum = "3e1a2532e4ed4ea13031c13bc7bc0dbca4aae32df48e9d77f0d1e743179f2ea1" dependencies = [ "lazy_static", "proc-macro2", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1135,7 +1135,7 @@ dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1255,7 +1255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1302,7 +1302,7 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1445,7 +1445,7 @@ checksum = "5c785274071b1b420972453b306eeca06acf4633829db4223b58a2a8c5953bc4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1721,7 +1721,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1796,7 +1796,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -1882,9 +1882,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -2237,7 +2237,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "903d476370e036f8d1560d4aed21ba366e9d83f02c1b27d9621aa1eaf99669b5" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -2895,7 +2895,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3046,9 +3046,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "percent-encoding" @@ -3079,7 +3079,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3212,12 +3212,12 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "prettyplease" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3241,9 +3241,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -3473,9 +3473,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -3521,9 +3521,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" [[package]] name = "rustybuzz" @@ -3559,9 +3559,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -3605,9 +3605,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" @@ -3626,7 +3626,7 @@ checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3648,7 +3648,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3945,7 +3945,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -3971,9 +3971,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" dependencies = [ "proc-macro2", "quote", @@ -4020,22 +4020,22 @@ checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4198,7 +4198,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.7", + "winnow 0.6.8", ] [[package]] @@ -4220,7 +4220,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4430,7 +4430,7 @@ checksum = "68c1b85ec843d3bc60e9d65fa7e00ce6549416a25c267b5ea93e6c81e3aa66e5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4476,7 +4476,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-shared", ] @@ -4510,7 +4510,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4767,7 +4767,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -4778,7 +4778,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] @@ -5055,9 +5055,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578" +checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d" dependencies = [ "memchr", ] @@ -5348,22 +5348,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.61", ] [[package]] From 0adb412793810e4e49b92c8c2f2432c2a7da76d7 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 7 May 2024 17:45:28 -0500 Subject: [PATCH 57/62] ModelRc now sortable! Ended up having to move some of the state managment for updating the order data of each DisplayMod to the back-end. Still seems better than doing a full deserialize of data read from file everytime a user changes order data. Also cleaned up callbacks for forcing the UI to re-render specific elements, this makes the UI work-around code look cleaner. --- src/main.rs | 204 ++++++++++++++++++++++++++---------- src/utils/ini/mod_loader.rs | 7 +- src/utils/installer.rs | 2 + ui/appwindow.slint | 2 + ui/common.slint | 4 +- ui/main.slint | 10 +- ui/tabs.slint | 52 ++++----- 7 files changed, 187 insertions(+), 94 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6a76a96..3a4e4cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,9 +15,9 @@ use elden_mod_loader_gui::{ }; use i_slint_backend_winit::WinitWindowAccessor; use log::{error, info, warn, debug}; -use slint::{ComponentHandle, Model, ModelRc, SharedString, VecModel, Timer}; +use slint::{ComponentHandle, Model, ModelRc, SharedString, Timer, VecModel}; use std::{ - collections::HashMap, ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ + collections::{HashMap, HashSet}, ffi::OsStr, io::ErrorKind, path::{Path, PathBuf}, rc::Rc, sync::{ atomic::{AtomicU32, Ordering}, OnceLock, } @@ -552,15 +552,11 @@ fn main() -> Result<(), slint::PlatformError> { let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { - // if error lets try it again and see if we can get sync-keys to cleanup any errors - match ini.collect_mods(None, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } - }), ui.as_weak() + }) + }),ui.as_weak() ); !state } @@ -694,13 +690,10 @@ fn main() -> Result<(), slint::PlatformError> { let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { - match ini.collect_mods(None, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } + }) }),ui.as_weak() ); } @@ -779,14 +772,11 @@ fn main() -> Result<(), slint::PlatformError> { }); let order_data = order_data_or_default(ui.as_weak(), None); deserialize_current_mods( - &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { - match ini.collect_mods(None, false) { - Ok(mods) => mods, - Err(err) => { - ui.display_msg(&err.to_string()); + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); vec![RegMod::default()] - } - } + }) }),ui.as_weak() ); }).unwrap(); @@ -943,7 +933,7 @@ fn main() -> Result<(), slint::PlatformError> { let load_orders = load_order.mut_section(); let stable_k = match state { true => { - load_orders.insert(&key, &value); + load_orders.insert(&key, &value.to_string()); Some(key.as_str()) } false => { @@ -958,12 +948,20 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); result = error; }); + let model = ui.global::().get_current_mods(); + let mut selected_mod = model.row_data(value as usize).unwrap(); + selected_mod.order.set = state; + model.set_row_data(value as usize, selected_mod); + if let Err(err) = model.update_order(&mut load_order, value, ui.as_weak()) { + ui.display_msg(&err.to_string()); + return error; + }; result } }); ui.global::().on_modify_order({ let ui_handle = ui.as_weak(); - move |to_k, from_k, value| -> i32 { + move |to_k, from_k, value, row, dll_i| -> i32 { let ui = ui_handle.unwrap(); let mut result = 0_i32; let cfg_dir = get_loader_ini_dir(); @@ -976,12 +974,12 @@ fn main() -> Result<(), slint::PlatformError> { }; let load_orders = load_order.mut_section(); if to_k != from_k && load_orders.contains_key(&from_k) { - load_orders.remove(from_k); - load_orders.append(&to_k, value) + load_orders.remove(&from_k); + load_orders.append(&to_k, value.to_string()) } else if load_orders.contains_key(&to_k) { - load_orders.insert(&to_k, value) + load_orders.insert(&to_k, value.to_string()) } else { - load_orders.append(&to_k, value); + load_orders.append(&to_k, value.to_string()); result = 1 }; @@ -989,6 +987,26 @@ fn main() -> Result<(), slint::PlatformError> { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); if !result.is_negative() { result = -1 } }); + if to_k != from_k { + let model = ui.global::().get_current_mods(); + let mut selected_mod = model.row_data(row as usize).unwrap(); + selected_mod.order.i = dll_i; + if !selected_mod.order.set { selected_mod.order.set = true } + model.set_row_data(row as usize, selected_mod); + if let Err(err) = model.update_order(&mut load_order, row, ui.as_weak()) { + ui.display_msg(&err.to_string()); + return -1; + }; + } else if value != row { + let model = ui.global::().get_current_mods(); + let mut curr_row = model.row_data(row as usize).unwrap(); + let mut replace_row = model.row_data(value as usize).unwrap(); + std::mem::swap(&mut curr_row.order.at, &mut replace_row.order.at); + model.set_row_data(row as usize, replace_row); + model.set_row_data(value as usize, curr_row); + ui.invoke_update_mod_index(value, 1); + ui.invoke_redraw_checkboxes(); + } result } }); @@ -1004,10 +1022,14 @@ fn main() -> Result<(), slint::PlatformError> { } }; let order_data = order_data_or_default(ui.as_weak(), None); - deserialize_current_mods(&ini.collect_mods(Some(&order_data), false).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] - }), ui.as_weak()); + deserialize_current_mods( + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }) + }),ui.as_weak() + ); info!("deserialized after encountered error"); } }); @@ -1016,6 +1038,76 @@ fn main() -> Result<(), slint::PlatformError> { ui.run() } +trait Sortable { + fn update_order(&self, cfg: &mut ModLoaderCfg, selected_row: i32, ui_handle: slint::Weak) -> std::io::Result<()>; +} + +impl Sortable for ModelRc { + fn update_order(&self, cfg: &mut ModLoaderCfg, selected_row: i32, ui_handle: slint::Weak) -> std::io::Result<()> { + let ui = ui_handle.unwrap(); + let order_map = cfg.parse_section()?; + + let mut unsorted_idx = (0..self.row_count()).collect::>(); + let selected_key = self.row_data(selected_row as usize).unwrap().name; + let mut i = 0_usize; + let mut selected_i = 0_i32; + let mut no_order_count = 0_usize; + let mut seen_names = HashSet::new(); + while !unsorted_idx.is_empty() { + if i >= unsorted_idx.len() { + i = 0 + } + let unsorted_i = unsorted_idx[i]; + let mut curr_row = self.row_data(unsorted_i).unwrap(); + let curr_key = curr_row.dll_files.row_data(curr_row.order.i as usize); + let new_order: Option<&usize>; + if curr_key.is_some() && {new_order = order_map.get(&curr_key.unwrap().to_string()); new_order}.is_some() { + let new_order = new_order.unwrap(); + curr_row.order.at = *new_order as i32 + 1; + if curr_row.name == selected_key { + selected_i = *new_order as i32; + } + if unsorted_i == *new_order { + self.set_row_data(*new_order, curr_row); + unsorted_idx.swap_remove(i); + continue; + } + if let Some(index) = unsorted_idx.iter().position(|&x| x == *new_order) { + let swap_row = self.row_data(*new_order).unwrap(); + if swap_row.name == selected_key { + selected_i = unsorted_i as i32; + } + self.set_row_data(*new_order, curr_row); + self.set_row_data(unsorted_i, swap_row); + unsorted_idx.swap_remove(index); + continue; + } + } else { + curr_row.order.at = 0; + if curr_row.dll_files.row_count() != 1 { + curr_row.order.i = -1; + } + if curr_row.name == selected_key { + selected_i = unsorted_i as i32; + } + if !seen_names.contains(&curr_row.name) { + seen_names.insert(curr_row.name.clone()); + no_order_count += 1; + } + self.set_row_data(unsorted_i, curr_row); + if no_order_count >= unsorted_idx.len() { + // alphabetical sort would go here + break; + } + i += 1; + } + } + ui.invoke_update_mod_index(selected_i, 1); + ui.invoke_redraw_checkboxes(); + Ok(()) + } +} + impl App { pub fn display_msg(&self, msg: &str) { self.set_display_message(SharedString::from(msg)); @@ -1204,17 +1296,13 @@ fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { ui.global::().set_orders_set(mods.order_count() as i32); } -// MARK: TODO -// need to use ModelNotify::row_changed to handle updating page info on change -// ui.invoke_update_mod_index(1, 1); - async fn install_new_mod( name: &str, files: Vec, game_dir: &Path, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); let mod_name = name.trim(); ui.display_confirm( &format!( @@ -1226,7 +1314,7 @@ async fn install_new_mod( return new_io_error!(ErrorKind::ConnectionAborted, "Mod install canceled"); } match InstallData::new(mod_name, files, game_dir) { - Ok(data) => add_dir_to_install_data(data, ui_weak).await, + Ok(data) => add_dir_to_install_data(data, ui_handle).await, Err(err) => Err(err) } } @@ -1235,24 +1323,24 @@ async fn install_new_files_to_mod( mod_data: &RegMod, files: Vec, game_dir: &Path, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm("Selected files are not installed? Would you like to try and install them?", true); if receive_msg().await != Message::Confirm { return new_io_error!(ErrorKind::ConnectionAborted, "Did not select to install files"); }; match InstallData::amend(mod_data, files, game_dir) { - Ok(data) => confirm_install(data, ui_weak).await, + Ok(data) => confirm_install(data, ui_handle).await, Err(err) => Err(err) } } async fn add_dir_to_install_data( mut install_files: InstallData, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm(&format!( "Current Files to install:\n{}\n\nWould you like to add a directory eg. Folder containing a config file?", install_files.display_paths), true); @@ -1280,7 +1368,7 @@ async fn add_dir_to_install_data( ui.display_msg(&format!("Error:\n\n{err}")); let _ = receive_msg().await; let future = async { - add_dir_to_install_data(install_files, ui_weak).await + add_dir_to_install_data(install_files, ui_handle).await }; let reselect_dir = Box::pin(future); reselect_dir.await @@ -1288,15 +1376,15 @@ async fn add_dir_to_install_data( new_io_error!(ErrorKind::Other, format!("Error: Could not Install\n\n{err}")) } } - true => confirm_install(install_files, ui_weak).await, + true => confirm_install(install_files, ui_handle).await, } } async fn confirm_install( install_files: InstallData, - ui_weak: slint::Weak, + ui_handle: slint::Weak, ) -> std::io::Result> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm( &format!( "Confirm install of mod \"{}\"\n\nSelected files:\n{}\n\nInstall at:\n{}", @@ -1321,9 +1409,9 @@ async fn confirm_install( } async fn confirm_remove_mod( - ui_weak: slint::Weak, + ui_handle: slint::Weak, game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); let install_dir = match reg_mod.files.file_refs().iter().min_by_key(|file| file.ancestors().count()) { Some(path) => game_dir.join(parent_or_err(path)?), None => PathBuf::from("Error: Failed to display a parent_dir"), @@ -1347,10 +1435,10 @@ fn mods_registered(ini: &ini::Ini) -> usize { } async fn confirm_scan_mods( - ui_weak: slint::Weak, + ui_handle: slint::Weak, game_dir: &Path, ini: Option) -> std::io::Result<()> { - let ui = ui_weak.unwrap(); + let ui = ui_handle.unwrap(); ui.display_confirm("Would you like to attempt to auto-import already installed mods to Elden Mod Loader GUI?", true); if receive_msg().await != Message::Confirm { @@ -1396,9 +1484,11 @@ async fn confirm_scan_mods( let mod_loader = ModLoader::properties(game_dir).unwrap_or_default(); let order_data = order_data_or_default(ui.as_weak(), Some(mod_loader.path())); deserialize_current_mods( - &new_ini.collect_mods(Some(&order_data), !mod_loader.installed()).unwrap_or_else(|err| { - ui.display_msg(&err.to_string()); - vec![RegMod::default()] + &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + ui.display_msg(&err.to_string()); + vec![RegMod::default()] + }) }),ui.as_weak() ); ui.display_msg(&format!("Successfully Found {len} mod(s)")); @@ -1412,7 +1502,7 @@ async fn confirm_scan_mods( let mut old_mods = ini.collect_mods(None, false)?; old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); if old_mods.is_empty() { return Ok(()) } - + let new_mods = new_ini.collect_mods(None, false)?; let all_new_dlls = new_mods.iter().flat_map(|m| m.files.dll_refs()).collect::>(); old_mods.retain(|m| !m.files.dll.iter().any(|f| all_new_dlls.contains(&f.as_path()))); diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index 2f77579..ff44bd0 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -135,7 +135,7 @@ impl ModLoaderCfg { } #[inline] - fn iter(&self) -> ini::PropertyIter { + pub fn iter(&self) -> ini::PropertyIter { self.section().iter() } @@ -174,6 +174,11 @@ impl ModLoaderCfg { self.section().is_empty() } + #[inline] + pub fn len(&self) -> usize { + self.section().len() + } + #[inline] pub fn path(&self) -> &Path { &self.dir diff --git a/src/utils/installer.rs b/src/utils/installer.rs index b8cbaef..57a5050 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -503,6 +503,8 @@ pub fn remove_mod_files(game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<() Ok(()) } })?; + // MARK: DEBUGME + // this isn't working if reg_mod.order.set { let file_name = file_name_or_err(®_mod.files.dll[reg_mod.order.i])?; remove_entry( diff --git a/ui/appwindow.slint b/ui/appwindow.slint index 04554b9..f8900be 100644 --- a/ui/appwindow.slint +++ b/ui/appwindow.slint @@ -44,7 +44,9 @@ export component App inherits Window { callback show-error-popup; callback show-confirm-popup; callback update-mod-index(int, int); + callback redraw-checkboxes; + redraw-checkboxes => { mp.redraw-checkboxes() } update-mod-index(i, t) => { mp.update-mod-index(i, t) } focus-app => { fs.focus() } show-error-popup => { diff --git a/ui/common.slint b/ui/common.slint index 6541e6d..5ad36ac 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -23,8 +23,8 @@ export global MainLogic { callback remove-mod(string); callback edit-config([string]); callback edit-config-item(StandardListViewItem); - callback add-remove-order(bool, string, string) -> int; - callback modify-order(string, string, string) -> int; + callback add-remove-order(bool, string, int) -> int; + callback modify-order(string, string, int, int, int) -> int; callback force-app-focus(); callback force-deserialize(); callback send-message(Message); diff --git a/ui/main.slint b/ui/main.slint index 2644fdd..428e379 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -3,6 +3,7 @@ import { SettingsPage, ModDetailsPage } from "sub-pages.slint"; import { MainLogic, SettingsLogic, Page, ColorPalette, Formatting } from "common.slint"; export component MainPage inherits Page { + property update-toggle: true; has-back-button: false; title: @tr("Mods"); description: @tr("Edit state of registered mods here"); @@ -15,6 +16,7 @@ export component MainPage inherits Page { callback swap-tab; callback edit-mod(int, int); callback update-mod-index(int, int); + callback redraw-checkboxes; focus-line-edit => { input-mod.focus() } focus-settings => { app-settings.focus-settings-scope() } swap-tab => { mod-settings.current-tab = mod-settings.current-tab == 0 ? 1 : 0 } @@ -25,6 +27,10 @@ export component MainPage inherits Page { mod-settings.mod-index = i; MainLogic.current-subpage = 2 } + redraw-checkboxes => { + update-toggle = false; + update-toggle = true; + } VerticalLayout { y: 27px; @@ -32,11 +38,11 @@ export component MainPage inherits Page { padding: Formatting.side-padding; padding-bottom: Formatting.side-padding / 2; - reg-mod-box := GroupBox { + if update-toggle : reg-mod-box := GroupBox { title: @tr("Registered-Mods:"); enabled: SettingsLogic.loader-installed && !SettingsLogic.loader-disabled; - list-view := ListView { + ListView { for mod[idx] in MainLogic.current-mods: re := Rectangle { height: 31px; border-radius: Formatting.rectangle-radius; diff --git a/ui/tabs.slint b/ui/tabs.slint index f59b876..d3b3c0e 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -44,6 +44,7 @@ export component ModDetails inherits Tab { export component ModEdit inherits Tab { in property mod-index; + property update-toggle: true; property has-config: MainLogic.current-mods[mod-index].config-files.length > 0; property selected-order: MainLogic.current-mods[mod-index].order.at; property selected-index: MainLogic.current-mods[mod-index].order.i; @@ -62,6 +63,7 @@ export component ModEdit inherits Tab { enabled: MainLogic.current-mods[mod-index].dll-files.length > 0 && SettingsLogic.loader-installed; function init-selected-index() { + MainLogic.current-mods[mod-index].order.at = 0; if MainLogic.current-mods[mod-index].dll-files.length != 1 { MainLogic.current-mods[mod-index].order.i = -1; } @@ -75,41 +77,43 @@ export component ModEdit inherits Tab { HorizontalLayout { row: 1; padding-top: Formatting.default-padding; + + function redraw-elements() { + update-toggle = false; + update-toggle = true; + } + load-order := Switch { text: @tr("Set Load Order"); - enabled: load-order-box.enabled; + enabled: MainLogic.current-mods[mod-index].dll-files.length > 0 && SettingsLogic.loader-installed; checked: MainLogic.current-mods[mod-index].order.set; toggled => { if self.checked { MainLogic.current-mods[mod-index].order.at = MainLogic.orders-set + 1; if selected-index != -1 && selected-order > 0 { // Front end `order.at` is 1 based and back end is 0 based - temp = MainLogic.add-remove-order(self.checked, selected-dll, selected-order - 1); + temp = MainLogic.add-remove-order(self.checked, selected-dll, mod-index); if temp != 42069 { MainLogic.orders-set = MainLogic.orders-set + temp; - MainLogic.current-mods[mod-index].order.set = self.checked; } else { self.checked = !self.checked; MainLogic.force-deserialize() } temp = 0 } + redraw-elements() } else { if selected-index != -1 && selected-order > 0 { - temp = MainLogic.add-remove-order(self.checked, selected-dll, selected-order - 1); + temp = MainLogic.add-remove-order(self.checked, selected-dll, mod-index); if temp != 42069 { MainLogic.orders-set = MainLogic.orders-set + temp; - init-selected-index() } else { self.checked = !self.checked; MainLogic.force-deserialize() } } - if temp != 42069 { - MainLogic.current-mods[mod-index].order.set = self.checked; - MainLogic.current-mods[mod-index].order.at = 0; - } - temp = 0 + temp = 0; + redraw-elements() } } } @@ -121,13 +125,9 @@ export component ModEdit inherits Tab { function modify-file(file: string, i: int) { if file != selected-dll && selected-order > 0 { - temp = MainLogic.modify-order(file, selected-dll, selected-order - 1); + temp = MainLogic.modify-order(file, selected-dll, selected-order - 1, mod-index, i); if temp != -1 { MainLogic.orders-set = MainLogic.orders-set + temp; - MainLogic.current-mods[mod-index].order.i = i; - if temp == 1 { - MainLogic.current-mods[mod-index].order.set = true; - } } else { init-selected-index(); MainLogic.force-deserialize() @@ -138,12 +138,14 @@ export component ModEdit inherits Tab { } function modify-index(v: int) { if selected-index != -1 && v > 0 { - temp = MainLogic.modify-order(selected-dll, selected-dll, v - 1); + temp = MainLogic.modify-order(selected-dll, selected-dll, v - 1, mod-index, selected-index); if temp != -1 { MainLogic.orders-set = MainLogic.orders-set + temp } else { MainLogic.force-deserialize() } + } else { + MainLogic.current-mods[mod-index].order.at = v } if temp != -1 { MainLogic.current-mods[mod-index].order.at = v @@ -152,13 +154,7 @@ export component ModEdit inherits Tab { } // Might be able to remove this hack after properly having sorting data parsed - if load-order.checked : ComboBox { - enabled: load-order.checked; - current-index: selected-index; - model: MainLogic.current-mods[mod-index].dll-files; - selected(file) => { modify-file(file, self.current-index) } - } - if !load-order.checked : ComboBox { + if update-toggle : ComboBox { enabled: load-order.checked; current-index: selected-index; model: MainLogic.current-mods[mod-index].dll-files; @@ -167,15 +163,7 @@ export component ModEdit inherits Tab { // MARK: TODO // Create a focus scope to handle up and down arrow inputs - if load-order.checked : SpinBox { - width: 106px; - enabled: load-order.checked; - minimum: 1; - maximum: MainLogic.orders-set; - value: selected-order; - edited(int) => { modify-index(int) } - } - if !load-order.checked : SpinBox { + if update-toggle : SpinBox { width: 106px; enabled: load-order.checked; minimum: 1; From 81384b6e2f83778fef1de57654ee20780584b313 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 7 May 2024 19:13:30 -0500 Subject: [PATCH 58/62] Bug fix fixed a bug where load_order data was not being passed along to mod removal function because of the changes to how collect_mods works --- src/main.rs | 20 ++++++++++++++++---- src/utils/installer.rs | 12 +++++++----- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3a4e4cc..a85f647 100644 --- a/src/main.rs +++ b/src/main.rs @@ -723,7 +723,19 @@ fn main() -> Result<(), slint::PlatformError> { if receive_msg().await != Message::Confirm { return } - let mut reg_mods = match ini.collect_mods(None, false) { + let order_map: Option>; + let loader = match ModLoaderCfg::read_section(get_loader_ini_dir(), LOADER_SECTIONS[1]) { + Ok(mut data) => { + order_map = data.parse_section().ok(); + data + }, + Err(err) => { + ui.display_msg(&err.to_string()); + order_map = None; + ModLoaderCfg::default() + } + }; + let mut reg_mods = match ini.collect_mods(order_map.as_ref(), false) { Ok(reg_mods) => reg_mods, Err(err) => { ui.display_msg(&err.to_string()); @@ -750,7 +762,7 @@ fn main() -> Result<(), slint::PlatformError> { remove_entry(ini_dir, INI_SECTIONS[2], &found_mod.name) .unwrap_or_else(|err| ui.display_msg(&err.to_string())); let ui_handle = ui.as_weak(); - match confirm_remove_mod(ui_handle, &game_dir, found_mod).await { + match confirm_remove_mod(ui_handle, &game_dir, loader.path(), found_mod).await { Ok(_) => ui.display_msg(&format!("Successfully removed all files associated with the previously registered mod \"{key}\"")), Err(err) => { match err.kind() { @@ -1410,7 +1422,7 @@ async fn confirm_install( async fn confirm_remove_mod( ui_handle: slint::Weak, - game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { + game_dir: &Path, loader_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { let ui = ui_handle.unwrap(); let install_dir = match reg_mod.files.file_refs().iter().min_by_key(|file| file.ancestors().count()) { Some(path) => game_dir.join(parent_or_err(path)?), @@ -1424,7 +1436,7 @@ async fn confirm_remove_mod( if receive_msg().await != Message::Confirm { return new_io_error!(ErrorKind::ConnectionAborted, format!("Mod files are still installed at \"{}\"", install_dir.display())); }; - remove_mod_files(game_dir, reg_mod) + remove_mod_files(game_dir, loader_dir, reg_mod) } /// returns the number of registered mods currently saved in the ".ini" diff --git a/src/utils/installer.rs b/src/utils/installer.rs index 57a5050..2173119 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -8,7 +8,6 @@ use std::{ use crate::{ does_dir_contain, file_name_or_err, new_io_error, parent_or_err, utils::ini::{ - mod_loader::ModLoader, parser::RegMod, writer::{remove_entry, save_bool, save_path, save_paths}, }, @@ -456,7 +455,11 @@ impl InstallData { } } -pub fn remove_mod_files(game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<()> { +pub fn remove_mod_files( + game_dir: &Path, + loader_path: &Path, + reg_mod: &RegMod, +) -> std::io::Result<()> { let remove_files = reg_mod .files .file_refs() @@ -503,12 +506,11 @@ pub fn remove_mod_files(game_dir: &Path, reg_mod: &RegMod) -> std::io::Result<() Ok(()) } })?; - // MARK: DEBUGME - // this isn't working + if reg_mod.order.set { let file_name = file_name_or_err(®_mod.files.dll[reg_mod.order.i])?; remove_entry( - ModLoader::properties(game_dir)?.path(), + loader_path, LOADER_SECTIONS[1], file_name.to_str().ok_or(std::io::Error::new( ErrorKind::InvalidData, From 9a04f776fc111e5630b7189657fba19dd1ca4f8b Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 7 May 2024 20:57:51 -0500 Subject: [PATCH 59/62] New colors for the light theme small bug fixes with scan for mods not working as intended polish for setting the correct order data to the user small optimization for ModelRc sorting --- src/main.rs | 24 +++++++++++++++--------- ui/common.slint | 8 ++++---- ui/main.slint | 4 ++-- ui/tabs.slint | 2 ++ 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index a85f647..6909813 100644 --- a/src/main.rs +++ b/src/main.rs @@ -963,6 +963,7 @@ fn main() -> Result<(), slint::PlatformError> { let model = ui.global::().get_current_mods(); let mut selected_mod = model.row_data(value as usize).unwrap(); selected_mod.order.set = state; + if !state { selected_mod.order.at = 0 } model.set_row_data(value as usize, selected_mod); if let Err(err) = model.update_order(&mut load_order, value, ui.as_weak()) { ui.display_msg(&err.to_string()); @@ -1060,7 +1061,7 @@ impl Sortable for ModelRc { let order_map = cfg.parse_section()?; let mut unsorted_idx = (0..self.row_count()).collect::>(); - let selected_key = self.row_data(selected_row as usize).unwrap().name; + let selected_key = self.row_data(selected_row as usize).expect("front end gives us a valid row").name; let mut i = 0_usize; let mut selected_i = 0_i32; let mut no_order_count = 0_usize; @@ -1070,7 +1071,7 @@ impl Sortable for ModelRc { i = 0 } let unsorted_i = unsorted_idx[i]; - let mut curr_row = self.row_data(unsorted_i).unwrap(); + let mut curr_row = self.row_data(unsorted_i).expect("unsorted_idx is valid ranges"); let curr_key = curr_row.dll_files.row_data(curr_row.order.i as usize); let new_order: Option<&usize>; if curr_key.is_some() && {new_order = order_map.get(&curr_key.unwrap().to_string()); new_order}.is_some() { @@ -1095,7 +1096,6 @@ impl Sortable for ModelRc { continue; } } else { - curr_row.order.at = 0; if curr_row.dll_files.row_count() != 1 { curr_row.order.i = -1; } @@ -1301,7 +1301,11 @@ fn deserialize_current_mods(mods: &[RegMod], ui_handle: slint::Weak) { files: ModelRc::from(files), config_files: ModelRc::from(config_files), dll_files: ModelRc::from(dll_files), - order: LoadOrder { at: mod_data.order.at as i32 + 1, i: mod_data.order.i as i32, set: mod_data.order.set }, + order: LoadOrder { + at: if !mod_data.order.set { 0 } else { mod_data.order.at as i32 + 1 }, + i: if !mod_data.order.set && mod_data.files.dll.len() != 1 { -1 } else { mod_data.order.i as i32 }, + set: mod_data.order.set + }, }) } ui.global::().set_current_mods(ModelRc::from(display_mods)); @@ -1440,7 +1444,6 @@ async fn confirm_remove_mod( } /// returns the number of registered mods currently saved in the ".ini" -/// this can't error if you pass in a `IniOption::Ini` fn mods_registered(ini: &ini::Ini) -> usize { let empty_ini = ini.section(INI_SECTIONS[2]).is_none() || ini.section(INI_SECTIONS[2]).unwrap().is_empty(); if empty_ini { 0 } else { ini.section(INI_SECTIONS[2]).unwrap().len() } @@ -1470,17 +1473,21 @@ async fn confirm_scan_mods( let num_registered = mods_registered(&ini.data); let empty_ini = num_registered == 0; + let mut old_mods: Vec; if !empty_ini { ui.display_confirm("Warning: This action will reset current registered mods, are you sure you want to continue?", true); if receive_msg().await != Message::Confirm { return Ok(()); }; + old_mods = ini.collect_mods(None, false)?; let dark_mode = ui.global::().get_dark_mode(); std::fs::remove_file(&ini.dir)?; new_cfg(&ini.dir)?; save_bool(&ini.dir, INI_SECTIONS[0], INI_KEYS[0], dark_mode)?; save_path(&ini.dir, INI_SECTIONS[1], INI_KEYS[1], game_dir)?; + } else { + old_mods = Vec::new(); } let new_ini: Cfg; match scan_for_mods(game_dir, &ini.dir) { @@ -1496,8 +1503,8 @@ async fn confirm_scan_mods( let mod_loader = ModLoader::properties(game_dir).unwrap_or_default(); let order_data = order_data_or_default(ui.as_weak(), Some(mod_loader.path())); deserialize_current_mods( - &ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { - ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { + &new_ini.collect_mods(Some(&order_data), false).unwrap_or_else(|_| { + new_ini.collect_mods( Some(&order_data), false).unwrap_or_else(|err| { ui.display_msg(&err.to_string()); vec![RegMod::default()] }) @@ -1510,8 +1517,7 @@ async fn confirm_scan_mods( new_ini = Cfg::default(); }, }; - if new_ini.dir != Path::new("") && num_registered != mods_registered(&new_ini.data) { - let mut old_mods = ini.collect_mods(None, false)?; + if new_ini.dir != Path::new("") && !old_mods.is_empty() && num_registered != mods_registered(&new_ini.data) { old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); if old_mods.is_empty() { return Ok(()) } diff --git a/ui/common.slint b/ui/common.slint index 5ad36ac..1338cbe 100644 --- a/ui/common.slint +++ b/ui/common.slint @@ -65,8 +65,8 @@ struct ButtonColors { // MARK: TODO // make light mode look not like shit export global ColorPalette { - out property page-background-color: SettingsLogic.dark-mode ? #1b1b1b : #60a0a4; - out property alt-page-background-color: SettingsLogic.dark-mode ? #132b4e : #747e81; + out property page-background-color: SettingsLogic.dark-mode ? #1b1b1b : #adbabb; + out property alt-page-background-color: SettingsLogic.dark-mode ? #132b4e : #38474e; out property popup-background-color: SettingsLogic.dark-mode ? #00393d : #1b1b1b; out property popup-border-color: SettingsLogic.dark-mode ? #17575c : #1b1b1b; @@ -77,12 +77,12 @@ export global ColorPalette { hovered: root.text-base.brighter(20%), }; - out property button-image-base: SettingsLogic.dark-mode ? #505150 : #aeaeae; + out property button-image-base: SettingsLogic.dark-mode ? #505150 : #ffffff; out property button-image-colors: { pressed: root.button-image-base.darker(40%), hovered: root.button-image-base.brighter(20%), }; - out property button-background-base: SettingsLogic.dark-mode ? #4b4b4b83 : #6fc5ffaf; + out property button-background-base: SettingsLogic.dark-mode ? #4b4b4b83 : #3e728b9e; out property button-background-colors: { pressed: root.button-background-base.darker(40%), hovered: root.button-background-base.darker(20%), diff --git a/ui/main.slint b/ui/main.slint index 428e379..e10635d 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -61,8 +61,8 @@ export component MainPage inherits Page { } } im := Image { - x: 274px; - y: 6px; + x: 282px; + y: 5px; image-fit: contain; height: 20px; source: @image-url("assets/arrow.png"); diff --git a/ui/tabs.slint b/ui/tabs.slint index d3b3c0e..3f1c04d 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -111,6 +111,8 @@ export component ModEdit inherits Tab { self.checked = !self.checked; MainLogic.force-deserialize() } + } else { + MainLogic.current-mods[mod-index].order.at = 0 } temp = 0; redraw-elements() From af3feb3ac1e7b2c61d91b2d9c9f671c4d0a1fd23 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Tue, 7 May 2024 22:15:35 -0500 Subject: [PATCH 60/62] bug fixes now removes load_order data for mods that were not able to be scanned in also fixed a bug so load_order doesn't fail on removal in the case of a mod with load_order data set not being able to be scanned or on mod deletion --- src/main.rs | 50 ++++++++++++++++++++++++------------- src/utils/ini/mod_loader.rs | 18 ++++++++++++- src/utils/ini/writer.rs | 17 +++++++++++-- src/utils/installer.rs | 18 +++++-------- 4 files changed, 71 insertions(+), 32 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6909813..90c71cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,10 +106,10 @@ fn main() -> Result<(), slint::PlatformError> { mod_loader_cfg = ModLoaderCfg::read_section(mod_loader.path(), LOADER_SECTIONS[1]).unwrap_or_else(|err| { debug!("error 4"); errors.push(err); - ModLoaderCfg::default() + ModLoaderCfg::default(mod_loader.path()) }); } else { - mod_loader_cfg = ModLoaderCfg::default(); + mod_loader_cfg = ModLoaderCfg::default(mod_loader.path()); } match mod_loader_cfg.parse_section() { Ok(data) => order_data = data, @@ -140,7 +140,7 @@ fn main() -> Result<(), slint::PlatformError> { Some(path) }, PathResult::Partial(path) | PathResult::None(path) => { - mod_loader_cfg = ModLoaderCfg::default(); + mod_loader_cfg = ModLoaderCfg::empty(); mod_loader = ModLoader::default(); order_data = HashMap::new(); game_verified = false; @@ -151,7 +151,7 @@ fn main() -> Result<(), slint::PlatformError> { // io::Write error debug!("error 8"); errors.push(err); - mod_loader_cfg = ModLoaderCfg::default(); + mod_loader_cfg = ModLoaderCfg::empty(); mod_loader = ModLoader::default(); order_data = HashMap::new(); game_verified = false; @@ -724,7 +724,8 @@ fn main() -> Result<(), slint::PlatformError> { return } let order_map: Option>; - let loader = match ModLoaderCfg::read_section(get_loader_ini_dir(), LOADER_SECTIONS[1]) { + let loader_dir = get_loader_ini_dir(); + let loader = match ModLoaderCfg::read_section(loader_dir, LOADER_SECTIONS[1]) { Ok(mut data) => { order_map = data.parse_section().ok(); data @@ -732,7 +733,7 @@ fn main() -> Result<(), slint::PlatformError> { Err(err) => { ui.display_msg(&err.to_string()); order_map = None; - ModLoaderCfg::default() + ModLoaderCfg::default(loader_dir) } }; let mut reg_mods = match ini.collect_mods(order_map.as_ref(), false) { @@ -1462,12 +1463,19 @@ async fn confirm_scan_mods( let ini = match ini { Some(data) => data, - None => match Cfg::read(get_ini_dir()) { - Ok(ini_data) => ini_data, - Err(err) => { - ui.display_msg(&err.to_string()); - return Err(err); - } + None => Cfg::read(get_ini_dir())?, + }; + let order_map: Option>; + let loader_dir = get_loader_ini_dir(); + let loader = match ModLoaderCfg::read_section(loader_dir, LOADER_SECTIONS[1]) { + Ok(mut data) => { + order_map = data.parse_section().ok(); + data + }, + Err(err) => { + ui.display_msg(&err.to_string()); + order_map = None; + ModLoaderCfg::default(loader_dir) } }; @@ -1479,7 +1487,7 @@ async fn confirm_scan_mods( if receive_msg().await != Message::Confirm { return Ok(()); }; - old_mods = ini.collect_mods(None, false)?; + old_mods = ini.collect_mods(order_map.as_ref(), false)?; let dark_mode = ui.global::().get_dark_mode(); std::fs::remove_file(&ini.dir)?; @@ -1518,12 +1526,20 @@ async fn confirm_scan_mods( }, }; if new_ini.dir != Path::new("") && !old_mods.is_empty() && num_registered != mods_registered(&new_ini.data) { - old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); + let new_mods = new_ini.collect_mods(None, false)?; + let all_new_files = new_mods.iter().flat_map(|m| m.files.file_refs()).collect::>(); + old_mods.retain(|m| !m.files.file_refs().iter().any(|&f| all_new_files.contains(f))); if old_mods.is_empty() { return Ok(()) } - let new_mods = new_ini.collect_mods(None, false)?; - let all_new_dlls = new_mods.iter().flat_map(|m| m.files.dll_refs()).collect::>(); - old_mods.retain(|m| !m.files.dll.iter().any(|f| all_new_dlls.contains(&f.as_path()))); + old_mods.iter().try_for_each(|m| { + if m.order.set { + remove_order_entry(m, loader.path()) + } else { + Ok(()) + } + })?; + + old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); if old_mods.is_empty() { return Ok(()) } old_mods.iter().try_for_each(|m| toggle_files(game_dir, true, m, None).map(|_| ()))?; diff --git a/src/utils/ini/mod_loader.rs b/src/utils/ini/mod_loader.rs index ff44bd0..e1fddae 100644 --- a/src/utils/ini/mod_loader.rs +++ b/src/utils/ini/mod_loader.rs @@ -75,7 +75,7 @@ impl ModLoader { } } -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ModLoaderCfg { data: Ini, dir: PathBuf, @@ -239,6 +239,22 @@ impl ModLoaderCfg { std::mem::swap(self.mut_section(), &mut new_section); self.write_to_file() } + + pub fn default(path: &Path) -> Self { + ModLoaderCfg { + data: ini::Ini::new(), + dir: PathBuf::from(path), + section: None, + } + } + + pub fn empty() -> Self { + ModLoaderCfg { + data: ini::Ini::new(), + dir: PathBuf::new(), + section: None, + } + } } pub trait Countable { diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index b4093ad..2a9ef2e 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -7,8 +7,9 @@ use std::{ }; use crate::{ - file_name_or_err, get_cfg, parent_or_err, DEFAULT_INI_VALUES, DEFAULT_LOADER_VALUES, INI_KEYS, - INI_SECTIONS, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, + file_name_or_err, get_cfg, parent_or_err, utils::ini::parser::RegMod, FileData, + DEFAULT_INI_VALUES, DEFAULT_LOADER_VALUES, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_KEYS, + LOADER_SECTIONS, }; const WRITE_OPTIONS: WriteOption = WriteOption { @@ -137,3 +138,15 @@ pub fn remove_entry(file_path: &Path, section: Option<&str>, key: &str) -> std:: ))?; config.write_to_file_opt(file_path, WRITE_OPTIONS) } + +pub fn remove_order_entry(entry: &RegMod, loader_dir: &Path) -> std::io::Result<()> { + let file_name = file_name_or_err(&entry.files.dll[entry.order.i])?; + let file_name = file_name.to_str().ok_or(std::io::Error::new( + ErrorKind::InvalidData, + format!("{file_name:?} is not valid UTF-8"), + ))?; + let file_data = FileData::from(file_name); + let file_name = format!("{}{}", file_data.name, file_data.extension); + remove_entry(loader_dir, LOADER_SECTIONS[1], &file_name)?; + Ok(()) +} diff --git a/src/utils/installer.rs b/src/utils/installer.rs index 2173119..9661c45 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -9,11 +9,13 @@ use crate::{ does_dir_contain, file_name_or_err, new_io_error, parent_or_err, utils::ini::{ parser::RegMod, - writer::{remove_entry, save_bool, save_path, save_paths}, + writer::{save_bool, save_path, save_paths}, }, - FileData, INI_SECTIONS, LOADER_SECTIONS, + FileData, INI_SECTIONS, }; +use super::ini::writer::remove_order_entry; + /// Returns the deepest occurance of a directory that contains at least 1 file /// Use parent_or_err for a direct binding to what is one level up fn get_parent_dir(input: &Path) -> std::io::Result { @@ -457,7 +459,7 @@ impl InstallData { pub fn remove_mod_files( game_dir: &Path, - loader_path: &Path, + loader_dir: &Path, reg_mod: &RegMod, ) -> std::io::Result<()> { let remove_files = reg_mod @@ -508,15 +510,7 @@ pub fn remove_mod_files( })?; if reg_mod.order.set { - let file_name = file_name_or_err(®_mod.files.dll[reg_mod.order.i])?; - remove_entry( - loader_path, - LOADER_SECTIONS[1], - file_name.to_str().ok_or(std::io::Error::new( - ErrorKind::InvalidData, - format!("{file_name:?} is not valid UTF-8"), - ))?, - )?; + remove_order_entry(reg_mod, loader_dir)?; } Ok(()) } From c9f4ca11101bf39e2126454375ed4c25c2ecc006 Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 8 May 2024 12:53:48 -0500 Subject: [PATCH 61/62] Code cleanup changed to a much more exhaustive check on scan for mods for determining if a previously registered dll file has not been found while scanning for mods fixed a bug in files_not_found() the success case was not computed correctly moved mods_registered to a impl fn for Cfg in lib.rs added mods_empty to reduce redundancy fixed a bug where mods_empty() was not being checked before running the scan_for_mods() fn fixed a bug where load_order ui elements enabled state were not computed correctly added comments --- src/lib.rs | 34 +++++++++++++++++++++--- src/main.rs | 58 ++++++++++++++++++++--------------------- src/utils/ini/parser.rs | 7 ++++- src/utils/ini/writer.rs | 19 +++++++------- src/utils/installer.rs | 7 ++--- ui/tabs.slint | 19 ++++++-------- 6 files changed, 87 insertions(+), 57 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 78c1403..3abec77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -194,6 +194,11 @@ pub fn toggle_files( Ok(short_path_new) } +// MARK: TODO +// make get_cfg() private and move over to get_or_setup_cfg() | would just need to figure out how to set the first startup flag + +/// If cfg file does not exist or is not set up with provided sections this function will +/// create a new ".ini" file in the given path pub fn get_or_setup_cfg(from_path: &Path, sections: &[Option<&str>]) -> std::io::Result { match get_cfg(from_path) { Ok(ini) => { @@ -262,13 +267,15 @@ where } } +/// returns a collection of references to entries in list that are not found in the supplied path +/// returns an empty Vec if all files were found pub fn files_not_found<'a, T>(in_path: &Path, list: &'a [&T]) -> std::io::Result> where T: std::borrow::Borrow + std::cmp::Eq + std::hash::Hash + ?Sized, for<'b> &'b str: std::borrow::Borrow, { match does_dir_contain(in_path, Operation::Count, list) { - Ok(OperationResult::Count((c, _))) if c == REQUIRED_GAME_FILES.len() => Ok(Vec::new()), + Ok(OperationResult::Count((c, _))) if c == list.len() => Ok(Vec::new()), Ok(OperationResult::Count((_, found_files))) => { Ok(list.iter().filter(|&&e| !found_files.contains(e)).copied().collect()) } @@ -311,6 +318,8 @@ impl FileData<'_> { } #[inline] + /// index is only used in the _disabled_ state to locate where `OFF_STATE` begins + /// saftey check to make sure `OFF_STATE` is found at the end of a `&str` fn state_data(path: &str) -> (bool, usize) { if let Some(index) = path.find(OFF_STATE) { (index != path.len() - OFF_STATE.len(), index) @@ -330,14 +339,14 @@ impl FileData<'_> { } } -/// Convience function to map Option None to an io Error +/// convience function to map Option None to an io Error pub fn parent_or_err(path: &Path) -> std::io::Result<&Path> { path.parent().ok_or(std::io::Error::new( ErrorKind::InvalidData, "Could not get parent_dir", )) } -/// Convience function to map Option None to an io Error +/// convience function to map Option None to an io Error pub fn file_name_or_err(path: &Path) -> std::io::Result<&std::ffi::OsStr> { path.file_name().ok_or(std::io::Error::new( ErrorKind::InvalidData, @@ -363,6 +372,7 @@ impl Cfg { dir: PathBuf::from(ini_path), } } + pub fn read(ini_path: &Path) -> std::io::Result { let data = get_or_setup_cfg(ini_path, &INI_SECTIONS)?; Ok(Cfg { @@ -376,6 +386,24 @@ impl Cfg { Ok(()) } + /// returns the number of registered mods currently saved in the ".ini" + pub fn mods_registered(&self) -> usize { + if self.data.section(INI_SECTIONS[2]).is_none() + || self.data.section(INI_SECTIONS[2]).unwrap().is_empty() + { + 0 + } else { + self.data.section(INI_SECTIONS[2]).unwrap().len() + } + } + + /// returns true if registered mods saved in the ".ini" is None + #[inline] + pub fn mods_empty(&self) -> bool { + self.data.section(INI_SECTIONS[2]).is_none() + || self.data.section(INI_SECTIONS[2]).unwrap().is_empty() + } + pub fn attempt_locate_game(&mut self) -> std::io::Result { if let Ok(path) = IniProperty::::read(&self.data, INI_SECTIONS[1], INI_KEYS[1], false) diff --git a/src/main.rs b/src/main.rs index 90c71cf..e0710b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ #![cfg(target_os = "windows")] // Setting windows_subsystem will hide console | cant read logs if console is hidden -// #![windows_subsystem = "windows"] +#![windows_subsystem = "windows"] use elden_mod_loader_gui::{ utils::{ @@ -129,7 +129,7 @@ fn main() -> Result<(), slint::PlatformError> { errors.push(err); } }; - if reg_mods.is_some() && reg_mods.as_ref().unwrap().len() != mods_registered(&ini.data) { + if reg_mods.is_some() && reg_mods.as_ref().unwrap().len() != ini.mods_registered() { ini = Cfg::read(current_ini).unwrap_or_else(|err| { debug!("error 7"); errors.push(err); @@ -280,7 +280,7 @@ fn main() -> Result<(), slint::PlatformError> { } else if game_verified { if !mod_loader.installed() { ui.display_msg(&format!("This tool requires Elden Mod Loader by TechieW to be installed!\n\nPlease install files to \"{}\"\nand relaunch Elden Mod Loader GUI", get_or_update_game_dir(None).display())); - } else if mods_registered(&ini.data) == 0 { + } else if ini.mods_empty() { if let Err(err) = confirm_scan_mods(ui.as_weak(), &game_dir.expect("game_verified"), Some(ini)).await { ui.display_msg(&err.to_string()); } @@ -491,9 +491,11 @@ fn main() -> Result<(), slint::PlatformError> { if mod_loader.installed() { ui.display_msg("Game Files Found!\nAdd mods to the app by entering a name and selecting mod files with \"Select Files\"\n\nYou can always add more files to a mod or de-register a mod at any time from within the app\n\nDo not forget to disable easy anti-cheat before playing with mods installed!"); let _ = receive_msg().await; - if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, Some(ini)).await { - ui.display_msg(&err.to_string()); - }; + if ini.mods_empty() { + if let Err(err) = confirm_scan_mods(ui.as_weak(), &try_path, Some(ini)).await { + ui.display_msg(&err.to_string()); + }; + } } else { ui.display_msg("Game Files Found!\n\nCould not find Elden Mod Loader Script!\nThis tool requires Elden Mod Loader by TechieW to be installed!") } @@ -935,7 +937,7 @@ fn main() -> Result<(), slint::PlatformError> { let ui = ui_handle.unwrap(); let error = 42069_i32; let cfg_dir = get_loader_ini_dir(); - let mut result: i32 = if state { 1 } else { -1 }; + let result: i32 = if state { 1 } else { -1 }; let mut load_order = match ModLoaderCfg::read_section(cfg_dir, LOADER_SECTIONS[1]) { Ok(data) => data, Err(err) => { @@ -957,10 +959,10 @@ fn main() -> Result<(), slint::PlatformError> { None } }; - load_order.update_order_entries(stable_k).unwrap_or_else(|err| { + if let Err(err) = load_order.update_order_entries(stable_k) { ui.display_msg(&format!("Failed to write to \"mod_loader_config.ini\"\n{err}")); - result = error; - }); + return error; + }; let model = ui.global::().get_current_mods(); let mut selected_mod = model.row_data(value as usize).unwrap(); selected_mod.order.set = state; @@ -1007,10 +1009,12 @@ fn main() -> Result<(), slint::PlatformError> { selected_mod.order.i = dll_i; if !selected_mod.order.set { selected_mod.order.set = true } model.set_row_data(row as usize, selected_mod); - if let Err(err) = model.update_order(&mut load_order, row, ui.as_weak()) { - ui.display_msg(&err.to_string()); - return -1; - }; + if value != row { + if let Err(err) = model.update_order(&mut load_order, row, ui.as_weak()) { + ui.display_msg(&err.to_string()); + return -1; + }; + } } else if value != row { let model = ui.global::().get_current_mods(); let mut curr_row = model.row_data(row as usize).unwrap(); @@ -1384,10 +1388,9 @@ async fn add_dir_to_install_data( if result.len() == 1 && err.kind() == ErrorKind::InvalidInput { ui.display_msg(&format!("Error:\n\n{err}")); let _ = receive_msg().await; - let future = async { + let reselect_dir = Box::pin(async { add_dir_to_install_data(install_files, ui_handle).await - }; - let reselect_dir = Box::pin(future); + }); reselect_dir.await } else { new_io_error!(ErrorKind::Other, format!("Error: Could not Install\n\n{err}")) @@ -1444,12 +1447,6 @@ async fn confirm_remove_mod( remove_mod_files(game_dir, loader_dir, reg_mod) } -/// returns the number of registered mods currently saved in the ".ini" -fn mods_registered(ini: &ini::Ini) -> usize { - let empty_ini = ini.section(INI_SECTIONS[2]).is_none() || ini.section(INI_SECTIONS[2]).unwrap().is_empty(); - if empty_ini { 0 } else { ini.section(INI_SECTIONS[2]).unwrap().len() } -} - async fn confirm_scan_mods( ui_handle: slint::Weak, game_dir: &Path, @@ -1479,10 +1476,8 @@ async fn confirm_scan_mods( } }; - let num_registered = mods_registered(&ini.data); - let empty_ini = num_registered == 0; let mut old_mods: Vec; - if !empty_ini { + if !ini.mods_empty() { ui.display_confirm("Warning: This action will reset current registered mods, are you sure you want to continue?", true); if receive_msg().await != Message::Confirm { return Ok(()); @@ -1525,20 +1520,23 @@ async fn confirm_scan_mods( new_ini = Cfg::default(); }, }; - if new_ini.dir != Path::new("") && !old_mods.is_empty() && num_registered != mods_registered(&new_ini.data) { + if new_ini.dir != Path::new("") && !old_mods.is_empty() { let new_mods = new_ini.collect_mods(None, false)?; let all_new_files = new_mods.iter().flat_map(|m| m.files.file_refs()).collect::>(); - old_mods.retain(|m| !m.files.file_refs().iter().any(|&f| all_new_files.contains(f))); + old_mods.retain(|m| m.files.dll.iter().any(|f| !all_new_files.contains(f.as_path()))); if old_mods.is_empty() { return Ok(()) } old_mods.iter().try_for_each(|m| { - if m.order.set { + if m.order.set && !all_new_files.contains(m.files.dll[m.order.i].as_path()) { remove_order_entry(m, loader.path()) } else { Ok(()) } })?; - + + old_mods.iter_mut().for_each(|m| m.files.dll.retain(|f| !all_new_files.contains(f.as_path()))); + old_mods.retain(|m| !m.files.dll.is_empty()); + if old_mods.is_empty() { return Ok(()) } old_mods.retain(|m| m.files.dll.iter().any(FileData::is_disabled)); if old_mods.is_empty() { return Ok(()) } diff --git a/src/utils/ini/parser.rs b/src/utils/ini/parser.rs index 0f1362e..844df02 100644 --- a/src/utils/ini/parser.rs +++ b/src/utils/ini/parser.rs @@ -332,7 +332,8 @@ pub struct LoadOrder { /// if one of `SplitFiles.dll` has a set load_order pub set: bool, - /// the index of the selected `mod_file` within `SplitFiles.dll` + /// the index of the selected `mod_file` within `SplitFiles.dll` + /// derialization will set this to -1 if `set` is false and `SplitFiles.dll` is not len 1 pub i: usize, /// current set value of `load_order` @@ -691,12 +692,14 @@ impl IntoIoError for ini::Error { } impl IntoIoError for std::str::ParseBoolError { + #[inline] fn into_io_error(self) -> std::io::Error { std::io::Error::new(ErrorKind::InvalidData, self.to_string()) } } impl IntoIoError for std::num::ParseIntError { + #[inline] fn into_io_error(self) -> std::io::Error { std::io::Error::new(ErrorKind::InvalidData, self.to_string()) } @@ -707,6 +710,7 @@ pub trait ModError { } impl ModError for std::io::Error { + #[inline] fn add_msg(self, msg: String) -> std::io::Error { std::io::Error::new(self.kind(), format!("{msg}\n\n{self}")) } @@ -717,6 +721,7 @@ pub trait ErrorClone { } impl ErrorClone for &std::io::Error { + #[inline] fn clone_err(self) -> std::io::Error { std::io::Error::new(self.kind(), self.to_string()) } diff --git a/src/utils/ini/writer.rs b/src/utils/ini/writer.rs index 2a9ef2e..d520acc 100644 --- a/src/utils/ini/writer.rs +++ b/src/utils/ini/writer.rs @@ -7,9 +7,9 @@ use std::{ }; use crate::{ - file_name_or_err, get_cfg, parent_or_err, utils::ini::parser::RegMod, FileData, - DEFAULT_INI_VALUES, DEFAULT_LOADER_VALUES, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_KEYS, - LOADER_SECTIONS, + file_name_or_err, get_cfg, parent_or_err, utils::ini::parser::RegMod, DEFAULT_INI_VALUES, + DEFAULT_LOADER_VALUES, INI_KEYS, INI_SECTIONS, LOADER_FILES, LOADER_KEYS, LOADER_SECTIONS, + OFF_STATE, }; const WRITE_OPTIONS: WriteOption = WriteOption { @@ -141,12 +141,13 @@ pub fn remove_entry(file_path: &Path, section: Option<&str>, key: &str) -> std:: pub fn remove_order_entry(entry: &RegMod, loader_dir: &Path) -> std::io::Result<()> { let file_name = file_name_or_err(&entry.files.dll[entry.order.i])?; - let file_name = file_name.to_str().ok_or(std::io::Error::new( - ErrorKind::InvalidData, - format!("{file_name:?} is not valid UTF-8"), - ))?; - let file_data = FileData::from(file_name); - let file_name = format!("{}{}", file_data.name, file_data.extension); + let file_name = file_name + .to_str() + .ok_or(std::io::Error::new( + ErrorKind::InvalidData, + format!("{file_name:?} is not valid UTF-8"), + ))? + .replace(OFF_STATE, ""); remove_entry(loader_dir, LOADER_SECTIONS[1], &file_name)?; Ok(()) } diff --git a/src/utils/installer.rs b/src/utils/installer.rs index 9661c45..5e17f69 100644 --- a/src/utils/installer.rs +++ b/src/utils/installer.rs @@ -9,13 +9,11 @@ use crate::{ does_dir_contain, file_name_or_err, new_io_error, parent_or_err, utils::ini::{ parser::RegMod, - writer::{save_bool, save_path, save_paths}, + writer::{remove_order_entry, save_bool, save_path, save_paths}, }, FileData, INI_SECTIONS, }; -use super::ini::writer::remove_order_entry; - /// Returns the deepest occurance of a directory that contains at least 1 file /// Use parent_or_err for a direct binding to what is one level up fn get_parent_dir(input: &Path) -> std::io::Result { @@ -35,6 +33,9 @@ fn get_parent_dir(input: &Path) -> std::io::Result { } } +// MARK: TODO +// create a directory_tree_is_empty() function for more efficent boolean checks + fn check_dir_contains_files(path: &Path) -> std::io::Result { let num_of_dirs = items_in_directory(path, FileType::Dir)?; if files_in_directory_tree(path)? == 0 { diff --git a/ui/tabs.slint b/ui/tabs.slint index 3f1c04d..7eea8f6 100644 --- a/ui/tabs.slint +++ b/ui/tabs.slint @@ -63,17 +63,14 @@ export component ModEdit inherits Tab { enabled: MainLogic.current-mods[mod-index].dll-files.length > 0 && SettingsLogic.loader-installed; function init-selected-index() { - MainLogic.current-mods[mod-index].order.at = 0; - if MainLogic.current-mods[mod-index].dll-files.length != 1 { - MainLogic.current-mods[mod-index].order.i = -1; - } - } - - init => { if !MainLogic.current-mods[mod-index].order.set { - init-selected-index() + MainLogic.current-mods[mod-index].order.at = 0; + if MainLogic.current-mods[mod-index].dll-files.length != 1 { + MainLogic.current-mods[mod-index].order.i = -1; + } } } + HorizontalLayout { row: 1; padding-top: Formatting.default-padding; @@ -85,7 +82,7 @@ export component ModEdit inherits Tab { load-order := Switch { text: @tr("Set Load Order"); - enabled: MainLogic.current-mods[mod-index].dll-files.length > 0 && SettingsLogic.loader-installed; + enabled: load-order-box.enabled; checked: MainLogic.current-mods[mod-index].order.set; toggled => { if self.checked { @@ -157,7 +154,7 @@ export component ModEdit inherits Tab { // Might be able to remove this hack after properly having sorting data parsed if update-toggle : ComboBox { - enabled: load-order.checked; + enabled: load-order.checked && load-order-box.enabled; current-index: selected-index; model: MainLogic.current-mods[mod-index].dll-files; selected(file) => { modify-file(file, self.current-index) } @@ -167,7 +164,7 @@ export component ModEdit inherits Tab { // Create a focus scope to handle up and down arrow inputs if update-toggle : SpinBox { width: 106px; - enabled: load-order.checked; + enabled: load-order.checked && load-order-box.enabled; minimum: 1; maximum: MainLogic.orders-set; value: selected-order; From 00056170ccfa89f2cf7d1662f03d43a840f36a2a Mon Sep 17 00:00:00 2001 From: Chad Lax Date: Wed, 8 May 2024 13:04:15 -0500 Subject: [PATCH 62/62] Pushed to v0.9.4 --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 4 ++-- build.rs | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ffbf628..b48e228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1405,7 +1405,7 @@ checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" [[package]] name = "elden_mod_loader_gui" -version = "0.9.3" +version = "0.9.4" dependencies = [ "criterion", "env_logger", @@ -1479,9 +1479,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3611,18 +3611,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.201" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" dependencies = [ "proc-macro2", "quote", @@ -3631,9 +3631,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", diff --git a/Cargo.toml b/Cargo.toml index 5b870bd..1c0ade1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "elden_mod_loader_gui" -version = "0.9.3" +version = "0.9.4" authors = ["WardLordRuby"] edition = "2021" build = "build.rs" @@ -10,7 +10,7 @@ build = "build.rs" [package.metadata.winresource] ProductName = "Elden Mod Loader GUI" FileDescription = "Elden Ring mod manager made by: WardLordRuby" -ProductVersion = "0.9.3-beta" +ProductVersion = "0.9.4-beta" [profile.release] opt-level = "z" # Optimize for size. diff --git a/build.rs b/build.rs index f7257ec..576c095 100644 --- a/build.rs +++ b/build.rs @@ -4,7 +4,7 @@ extern crate winresource; /// `MAJOR << 48 | MINOR << 32 | PATCH << 16 | RELEASE` const MAJOR: u64 = 0; const MINOR: u64 = 9; -const PATCH: u64 = 3; +const PATCH: u64 = 4; const RELEASE: u64 = 2; fn main() {