├── .github └── workflows │ ├── check.yml │ └── rust.yml ├── .gitignore ├── .gitmodules ├── README.md ├── assets ├── Screenshot 2023-11-10 173502.png ├── Screenshot 2023-11-28 093042.png ├── Screenshot 2024-02-20 102133.png ├── red4-update.png ├── red4_conflicts_01.png ├── red4_conflicts_02.png └── red4_conflicts_03.png ├── red4-conflicts.code-workspace ├── red4-conflicts ├── .gitignore ├── .vscode │ └── launch.json ├── Cargo.lock ├── Cargo.toml ├── assets │ ├── favicon.ico │ ├── icon-1024.png │ ├── icon-256.png │ ├── icon_ios_touch_192.png │ ├── manifest.json │ ├── maskable_icon_x512.png │ └── sw.js └── src │ ├── app.rs │ ├── lib.rs │ └── main.rs ├── red4-tweak-browser.code-workspace ├── red4-tweak-browser ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── assets │ ├── favicon.ico │ ├── icon-1024.png │ ├── icon-256.png │ ├── icon_ios_touch_192.png │ ├── manifest.json │ ├── maskable_icon_x512.png │ └── sw.js └── src │ ├── app.rs │ ├── lib.rs │ └── main.rs ├── red4-update.code-workspace ├── red4-update ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── report.json └── src │ ├── diff.json │ ├── lib.rs │ └── main.rs ├── tweak-doxygen.code-workspace └── tweak-doxygen ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── src └── main.rs /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | # automated checks 12 | check: 13 | runs-on: ${{ matrix.os }} 14 | 15 | strategy: 16 | # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. 17 | fail-fast: true 18 | matrix: 19 | os: [windows-latest] 20 | build_type: [release] 21 | target: [tweak-doxygen, red4-tweak-browser, red4-conflicts, "red4-update"] 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | 26 | - name: Build 27 | run: | 28 | cd ${{ matrix.target }} 29 | cargo build --${{ matrix.build_type }} 30 | - name: Run tests 31 | run: | 32 | cd ${{ matrix.target }} 33 | cargo test -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | # automated checks 12 | check: 13 | runs-on: ${{ matrix.os }} 14 | 15 | permissions: 16 | contents: write 17 | 18 | strategy: 19 | # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. 20 | fail-fast: true 21 | matrix: 22 | os: [windows-latest] 23 | build_type: [release] 24 | target: [tweak-doxygen, red4-tweak-browser, red4-conflicts, "red4-update"] 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: Build 30 | run: | 31 | cd ${{ matrix.target }} 32 | cargo build --${{ matrix.build_type }} 33 | - name: Run tests 34 | run: | 35 | cd ${{ matrix.target }} 36 | cargo test 37 | 38 | - name: Upload a Build Artifact 39 | uses: actions/upload-artifact@v3 40 | with: 41 | name: ${{ matrix.target }} 42 | path: ${{ matrix.target }}/target/${{ matrix.build_type }}/${{ matrix.target }}.exe 43 | 44 | - name: zip 45 | run: Compress-Archive -Path "${{ matrix.target }}/target/${{ matrix.build_type }}/${{ matrix.target }}.exe" -DestinationPath "${{ matrix.target }}.zip" 46 | 47 | - name: Upload to release 48 | uses: ncipollo/release-action@v1 49 | with: 50 | artifacts: "${{ matrix.target }}.zip" 51 | allowUpdates: true 52 | tag: "latest" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | red4-conflicts/src/metadata-resources.csv 4 | red4-conflicts/red4-conflicts.log 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/.gitmodules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Rust](https://github.com/rfuzzo/Cyberpunk-utility/actions/workflows/rust.yml/badge.svg)](https://github.com/rfuzzo/Cyberpunk-utility/actions/workflows/rust.yml) 2 | 3 | # Cyberpunk-utility 4 | 5 | Some utility tools for Cyberpunk 2077 modding. 6 | 7 | ## RED4-Conflicts 8 | 9 | > Nexus Mods link: https://www.nexusmods.com/cyberpunk2077/mods/11126 10 | 11 | A conflict-checker app for Cyberpunk 2077 archives. 12 | 13 | ### Usage 14 | - download and extract 15 | - run `red4-conflicts.exe` and specify a folder with archives to check 16 | 17 | ### Screenshots 18 | ![screenshot](./assets/red4_conflicts_02.png) 19 | 20 | ## RED4-Update 21 | 22 | A commandline tool to check if a mod needs an update after a game patch 23 | 24 | ### Usage 25 | - download and extract 26 | - run `red4-update.exe` and specify a folder with archives to check (or run it from within that folder) 27 | 28 | ```cmd 29 | Usage: red4-update.exe check [PATH] 30 | 31 | Arguments: 32 | [PATH] Path to a folder with archives to check 33 | 34 | Options: 35 | -h, --help Print help 36 | ``` 37 | 38 | ### Screenshots 39 | ![screenshot](./assets/red4-update.png) 40 | 41 | ## Cyberpunk-Tweak Util 42 | A small app to display various tweak-related info. 43 | 44 | ### Usage 45 | - download and extract 46 | - run `red4-tweak_browser.exe` and 47 | - fill in the path to the REDmod tweak sources when prompted (e.g. `D:\games\Cyberpunk 2077\tools\redmod\tweaks`) 48 | - hit "Generate" when prompted 49 | 50 | ### Screenshots 51 | ![screenshot](./assets/Screenshot%202023-11-10%20173502.png) 52 | 53 | ## Tweak-Doxygen 54 | 55 | A small rust utility to convert and strip tweak records () to a c# class hierarchy for use with doxygen: 56 | 57 | ### Usage 58 | ```cmd 59 | tweakdox 60 | ``` 61 | -------------------------------------------------------------------------------- /assets/Screenshot 2023-11-10 173502.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/Screenshot 2023-11-10 173502.png -------------------------------------------------------------------------------- /assets/Screenshot 2023-11-28 093042.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/Screenshot 2023-11-28 093042.png -------------------------------------------------------------------------------- /assets/Screenshot 2024-02-20 102133.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/Screenshot 2024-02-20 102133.png -------------------------------------------------------------------------------- /assets/red4-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/red4-update.png -------------------------------------------------------------------------------- /assets/red4_conflicts_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/red4_conflicts_01.png -------------------------------------------------------------------------------- /assets/red4_conflicts_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/red4_conflicts_02.png -------------------------------------------------------------------------------- /assets/red4_conflicts_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/assets/red4_conflicts_03.png -------------------------------------------------------------------------------- /red4-conflicts.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "red4-conflicts" 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /red4-conflicts/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /dist 3 | -------------------------------------------------------------------------------- /red4-conflicts/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'red4-conflicts'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=red4-conflicts", 15 | "--package=red4-conflicts" 16 | ], 17 | "filter": { 18 | "name": "red4-conflicts", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | ] 26 | } -------------------------------------------------------------------------------- /red4-conflicts/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "red4-conflicts" 3 | version = "0.5.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | egui = "0.29" 8 | eframe = { version = "0.29", default-features = false, features = [ 9 | "default_fonts", # Embed the default egui fonts. 10 | "glow", # Use the glow rendering backend. Alternative: "wgpu". 11 | "persistence", # Enable restoring app state when restarting the app. 12 | ] } 13 | log = "0.4" 14 | rfd = "0.15" 15 | serde = { version = "1", features = ["derive"] } 16 | simple-logging = "2.0" 17 | open = "5.3" 18 | egui_dnd = "0.10" 19 | 20 | [patch.crates-io] 21 | 22 | [dependencies.red4lib] 23 | git = "https://github.com/rfuzzo/red4lib" 24 | #path = "D:\\GitHub\\__rfuzzo\\red4lib" 25 | -------------------------------------------------------------------------------- /red4-conflicts/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-conflicts/assets/favicon.ico -------------------------------------------------------------------------------- /red4-conflicts/assets/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-conflicts/assets/icon-1024.png -------------------------------------------------------------------------------- /red4-conflicts/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-conflicts/assets/icon-256.png -------------------------------------------------------------------------------- /red4-conflicts/assets/icon_ios_touch_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-conflicts/assets/icon_ios_touch_192.png -------------------------------------------------------------------------------- /red4-conflicts/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egui Template PWA", 3 | "short_name": "egui-template-pwa", 4 | "icons": [ 5 | { 6 | "src": "./icon-256.png", 7 | "sizes": "256x256", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./maskable_icon_x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "any maskable" 15 | }, 16 | { 17 | "src": "./icon-1024.png", 18 | "sizes": "1024x1024", 19 | "type": "image/png" 20 | } 21 | ], 22 | "lang": "en-US", 23 | "id": "/index.html", 24 | "start_url": "./index.html", 25 | "display": "standalone", 26 | "background_color": "white", 27 | "theme_color": "white" 28 | } 29 | -------------------------------------------------------------------------------- /red4-conflicts/assets/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-conflicts/assets/maskable_icon_x512.png -------------------------------------------------------------------------------- /red4-conflicts/assets/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'egui-template-pwa'; 2 | var filesToCache = [ 3 | './', 4 | './index.html', 5 | './plox_gui.js', 6 | './plox_gui_bg.wasm', 7 | ]; 8 | 9 | /* Start the service worker and cache all of the app's content */ 10 | self.addEventListener('install', function (e) { 11 | e.waitUntil( 12 | caches.open(cacheName).then(function (cache) { 13 | return cache.addAll(filesToCache); 14 | }) 15 | ); 16 | }); 17 | 18 | /* Serve cached content when offline */ 19 | self.addEventListener('fetch', function (e) { 20 | e.respondWith( 21 | caches.match(e.request).then(function (response) { 22 | return response || fetch(e.request); 23 | }) 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /red4-conflicts/src/app.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, env}; 2 | 3 | use egui::Color32; 4 | use red4lib::{fnv1a64_hash_path, get_red4_hashes}; 5 | 6 | use crate::{ArchiveViewModel, ETooltipVisuals, TemplateApp}; 7 | 8 | impl eframe::App for TemplateApp { 9 | /// Called by the frame work to save state before shutdown. 10 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 11 | eframe::set_value(storage, eframe::APP_KEY, self); 12 | } 13 | 14 | /// Called each time the UI needs repainting, which may be many times per second. 15 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 16 | // first time, load hashes 17 | if self.hashes.is_empty() { 18 | self.hashes = get_red4_hashes(); 19 | } 20 | // first time, set game path to cwd 21 | if !self.game_path.exists() { 22 | if let Ok(current_dir) = env::current_dir() { 23 | self.game_path = current_dir; 24 | } 25 | } 26 | 27 | // each frame we check the load order 28 | 29 | // auto-generate hashes on first load and load order change 30 | if let Some(last_load_order) = &self.last_load_order { 31 | if &self.load_order != last_load_order { 32 | self.generate_conflict_map(); 33 | self.last_load_order = Some(self.load_order.clone()); 34 | self.serialize_load_order(); 35 | } 36 | } else { 37 | // first load 38 | self.reload_load_order(); 39 | self.generate_conflict_map(); 40 | self.last_load_order = Some(self.load_order.clone()); 41 | self.serialize_load_order(); 42 | } 43 | 44 | // Menu bar 45 | egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { 46 | self.menu_bar_view(ui, ctx); 47 | }); 48 | 49 | // Left panel 50 | egui::SidePanel::left("left_panel").show(ctx, |ui| { 51 | self.load_order_view(ui); 52 | }); 53 | 54 | // main conflicts view 55 | egui::CentralPanel::default().show(ctx, |ui| { 56 | self.conflicts_view(ui); 57 | }); 58 | 59 | 60 | } 61 | } 62 | 63 | impl TemplateApp { 64 | /// Side panel with a mod list in correct order 65 | fn load_order_view(&mut self, ui: &mut egui::Ui) { 66 | ui.heading("Load Order"); 67 | ui.label("Drag to reorder, higher overrides"); 68 | ui.horizontal(|ui| { 69 | ui.checkbox(&mut self.enable_modlist, "Enable load order re-ordering"); 70 | let response = ui.button("?"); 71 | let popup_id = ui.make_persistent_id("my_unique_id"); 72 | if response.clicked() { 73 | ui.memory_mut(|mem| mem.toggle_popup(popup_id)); 74 | } 75 | // open info 76 | let below = egui::AboveOrBelow::Below; 77 | let close_on_click_outside = egui::popup::PopupCloseBehavior::CloseOnClickOutside; 78 | egui::popup::popup_above_or_below_widget(ui, popup_id, &response, below, close_on_click_outside, |ui| { 79 | ui.set_min_width(400.0); // if you want to control the size 80 | ui.heading("Cyberpunk 2077 load order"); 81 | ui.label("Archives in Cyberpunk are loaded binary-alphabetically."); 82 | ui.label("This means that a mod called \"modaa\" loads before \"modbb\", but \"modA\" loads before \"modaa\" and \"modbb\"."); 83 | ui.label("Special characters also load according to binary sorting: \"!\" and \"#\" before \"A\", but \"_\" after \"Z\". Check the ASCII character set for more info:"); 84 | ui.hyperlink("https://en.wikipedia.org/wiki/ASCII#Character_set/"); 85 | ui.label("All REDmod archives are strictly loaded after archives in the /archive/pc/mod folder."); 86 | 87 | ui.add_space(16.0); 88 | ui.heading("Modlist.txt"); 89 | ui.label("The game provides a way to adjust archive load order without renaming the files: Archives in \"modlist.txt\" in your /archive/pc/mod folder are loaded according to this list."); 90 | ui.label("Reordering mods in this app will generate this file."); 91 | }); 92 | }); 93 | 94 | ui.separator(); 95 | 96 | egui::ScrollArea::vertical().show(ui, |ui| { 97 | if self.enable_modlist { 98 | egui_dnd::dnd(ui, "mod_list_dnd").show_vec( 99 | &mut self.load_order, 100 | |ui, f, handle, _state| { 101 | ui.horizontal(|ui| { 102 | handle.ui(ui, |ui| { 103 | ui.label("::"); 104 | }); 105 | ui.label(f.clone()); 106 | }); 107 | }, 108 | ); 109 | } else { 110 | egui::Grid::new("mod_list").show(ui, |ui| { 111 | let mods = &self.load_order; 112 | for f in mods.iter() { 113 | ui.label(f); 114 | ui.end_row(); 115 | } 116 | }); 117 | } 118 | }); 119 | } 120 | 121 | /// Main conflict grid 122 | fn conflicts_view(&mut self, ui: &mut egui::Ui) { 123 | ui.heading("Conflicts"); 124 | ui.separator(); 125 | // ------------------- 126 | ui.horizontal(|ui| { 127 | ui.label("Archives path"); 128 | if let Some(mut path_str) = self.game_path.to_str() { 129 | ui.text_edit_singleline(&mut path_str); 130 | } 131 | if ui.button("...").clicked() { 132 | // open file 133 | if let Some(folder) = rfd::FileDialog::new().set_directory("/").pick_folder() { 134 | self.game_path = folder; 135 | // regenerate conflicts 136 | self.last_load_order = None; 137 | } 138 | } 139 | // generate conflict map 140 | if ui.button("⟳ Re-check conflicts").clicked() && self.game_path.exists() { 141 | self.reload_load_order(); 142 | self.generate_conflict_map(); 143 | self.last_load_order = Some(self.load_order.clone()); 144 | } 145 | if ui.button("🗁 Open in Explorer").clicked() && self.game_path.exists() { 146 | let _ = open::that(self.game_path.clone()); 147 | } 148 | }); 149 | ui.separator(); 150 | // ------------------- 151 | // Toolbar 152 | ui.horizontal(|ui| { 153 | ui.checkbox(&mut self.show_no_conflicts, "Show not conflicting files"); 154 | ui.label("Conflict style"); 155 | egui::ComboBox::from_id_salt("tooltips_visuals") 156 | .selected_text(format!("{:?}", &mut self.tooltips_visuals)) 157 | .show_ui(ui, |ui| { 158 | ui.selectable_value( 159 | &mut self.tooltips_visuals, 160 | ETooltipVisuals::Tooltip, 161 | "Tooltip", 162 | ); 163 | ui.selectable_value( 164 | &mut self.tooltips_visuals, 165 | ETooltipVisuals::Inline, 166 | "Inline", 167 | ); 168 | ui.selectable_value( 169 | &mut self.tooltips_visuals, 170 | ETooltipVisuals::Collapsing, 171 | "Collapsing", 172 | ); 173 | }); 174 | }); 175 | // Filters 176 | ui.horizontal(|ui| { 177 | ui.label("Mod filter: "); 178 | ui.text_edit_singleline(&mut self.text_filter); 179 | if ui.button("x").clicked() { 180 | self.text_filter.clear(); 181 | } 182 | ui.separator(); 183 | ui.label("File filter: "); 184 | ui.text_edit_singleline(&mut self.file_filter); 185 | if ui.button("x").clicked() { 186 | self.file_filter.clear(); 187 | } 188 | }); 189 | ui.label(format!( 190 | "Found {} conflicts across {} archives", 191 | self.conflicts.len(), 192 | self.load_order.len() 193 | )); 194 | 195 | ui.separator(); 196 | 197 | egui::ScrollArea::both() 198 | .auto_shrink([false; 2]) 199 | .show(ui, |ui| { 200 | egui::Grid::new("mod_list").num_columns(2).show(ui, |ui| { 201 | for archive_name in &self.load_order { 202 | let archive_path = &self.game_path.join(archive_name); 203 | let k = &fnv1a64_hash_path(archive_path); 204 | if let Some(mod_vm) = self.archives.get(k) { 205 | // skip if no conflicts 206 | if mod_vm.loses.len() + mod_vm.wins.len() == 0 { 207 | continue; 208 | } 209 | 210 | // text filter 211 | if !self.text_filter.is_empty() 212 | && !mod_vm.file_name 213 | .to_lowercase() 214 | .contains(&self.text_filter.to_lowercase()) 215 | { 216 | continue; 217 | } 218 | 219 | let filename_ext = if !self.show_no_conflicts { 220 | format!( 221 | "{} (w: {}, l: {})", 222 | mod_vm.file_name, 223 | mod_vm.wins.len(), 224 | mod_vm.loses.len() 225 | ) 226 | } else { 227 | format!( 228 | "{} (w: {}, l: {}, u: {})", 229 | mod_vm.file_name, 230 | mod_vm.wins.len(), 231 | mod_vm.loses.len(), 232 | mod_vm.get_no_conflicts().len() 233 | ) 234 | }; 235 | 236 | // column 1 237 | ui.collapsing(filename_ext, |ui| { 238 | let mut header_color = if mod_vm.wins.is_empty() { 239 | ui.visuals().text_color() 240 | } else { 241 | Color32::GREEN 242 | }; 243 | ui.collapsing( 244 | egui::RichText::new(format!("winning ({})", mod_vm.wins.len())) 245 | .color(header_color), 246 | |ui| { 247 | for h in &mod_vm.wins { 248 | // resolve hash 249 | let mut label_text = h.to_string(); 250 | if let Some(file_name) = self.hashes.get(h) { 251 | label_text = file_name.to_owned(); 252 | } 253 | 254 | // text filter 255 | if !self.file_filter.is_empty() 256 | && !label_text 257 | .to_lowercase() 258 | .contains(&self.file_filter.to_lowercase()) 259 | { 260 | continue; 261 | } 262 | 263 | match self.tooltips_visuals { 264 | crate::ETooltipVisuals::Tooltip => { 265 | show_tooltip( 266 | ui, 267 | label_text, 268 | h, 269 | k, 270 | &self.conflicts, 271 | &self.archives, 272 | true, 273 | ); 274 | } 275 | crate::ETooltipVisuals::Inline => { 276 | show_inline( 277 | ui, 278 | label_text, 279 | h, 280 | k, 281 | &self.conflicts, 282 | &self.archives, 283 | true 284 | ); 285 | } 286 | crate::ETooltipVisuals::Collapsing => { 287 | show_dropdown_filelist( 288 | ui, 289 | label_text, 290 | h, 291 | k, 292 | &self.conflicts, 293 | &self.archives, 294 | true, 295 | ); 296 | } 297 | } 298 | } 299 | }, 300 | ); 301 | 302 | header_color = if mod_vm.loses.is_empty() { 303 | ui.visuals().text_color() 304 | } else { 305 | Color32::RED 306 | }; 307 | ui.collapsing( 308 | egui::RichText::new(format!("losing ({})", mod_vm.loses.len())) 309 | .color(header_color), 310 | |ui| { 311 | for h in &mod_vm.loses { 312 | let mut label_text = h.to_string(); 313 | if let Some(file_name) = self.hashes.get(h) { 314 | label_text = file_name.to_owned(); 315 | } 316 | 317 | // text filter 318 | if !self.file_filter.is_empty() 319 | && !label_text 320 | .to_lowercase() 321 | .contains(&self.file_filter.to_lowercase()) 322 | { 323 | continue; 324 | } 325 | 326 | match self.tooltips_visuals { 327 | crate::ETooltipVisuals::Tooltip => { 328 | show_tooltip( 329 | ui, 330 | label_text, 331 | h, 332 | k, 333 | &self.conflicts, 334 | &self.archives, 335 | false, 336 | ); 337 | } 338 | crate::ETooltipVisuals::Inline => { 339 | show_inline( 340 | ui, 341 | label_text, 342 | h, 343 | k, 344 | &self.conflicts, 345 | &self.archives, 346 | false 347 | ); 348 | } 349 | crate::ETooltipVisuals::Collapsing => { 350 | show_dropdown_filelist( 351 | ui, 352 | label_text, 353 | h, 354 | k, 355 | &self.conflicts, 356 | &self.archives, 357 | false, 358 | ); 359 | } 360 | } 361 | } 362 | }, 363 | ); 364 | 365 | if self.show_no_conflicts { 366 | ui.collapsing( 367 | format!( 368 | "no conflicts ({})", 369 | mod_vm.get_no_conflicts().len() 370 | ), 371 | |ui| { 372 | for h in &mod_vm.get_no_conflicts() { 373 | let mut label_text = h.to_string(); 374 | if let Some(file_name) = self.hashes.get(h) { 375 | label_text = file_name.to_owned(); 376 | } 377 | ui.add(egui::Label::new(label_text).wrap_mode(egui::TextWrapMode::Truncate)); 378 | } 379 | }, 380 | ); 381 | } 382 | }); 383 | 384 | // column 2 385 | ui.horizontal_top(|ui| 386 | { 387 | // if all files of a mod are losing then its obsolete 388 | if (mod_vm.files.len() == mod_vm.loses.len()) && mod_vm.wins.is_empty() { 389 | ui.colored_label( Color32::GRAY, "⏺"); 390 | } 391 | else { 392 | // if some files are winning add green dot 393 | if !mod_vm.wins.is_empty() { 394 | ui.colored_label( Color32::GREEN, "⏺"); 395 | } 396 | // if some files are losing add red dot 397 | if !mod_vm.loses.is_empty() { 398 | ui.colored_label( Color32::RED, "⏺"); 399 | } 400 | } 401 | }); 402 | 403 | ui.end_row(); 404 | 405 | } 406 | } 407 | }); 408 | }); 409 | } 410 | 411 | /// The menu bar 412 | fn menu_bar_view(&mut self, ui: &mut egui::Ui, ctx: &egui::Context) { 413 | // The top panel is often a good place for a menu bar: 414 | egui::menu::bar(ui, |ui| { 415 | ui.menu_button("File", |ui| { 416 | if ui.button("Open modlist.txt").clicked() { 417 | let _ = open::that(self.game_path.join("modlist.txt")); 418 | ui.close_menu(); 419 | } 420 | ui.separator(); 421 | if ui.button("Quit").clicked() { 422 | ctx.send_viewport_cmd(egui::ViewportCommand::Close); 423 | } 424 | }); 425 | ui.menu_button("About", |ui| { 426 | ui.hyperlink("https://github.com/rfuzzo/Cyberpunk-utility/"); 427 | ui.separator(); 428 | if ui.button("Open log").clicked() { 429 | let _ = open::that(format!("{}.log", crate::CARGO_PKG_NAME)); 430 | 431 | ui.close_menu(); 432 | } 433 | }); 434 | ui.add_space(16.0); 435 | 436 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 437 | egui::global_theme_preference_buttons(ui); 438 | 439 | ui.add_space(16.0); 440 | 441 | ui.separator(); 442 | egui::warn_if_debug_build(ui); 443 | ui.label(format!("v{}", crate::CARGO_PKG_VERSION)); 444 | }); 445 | }); 446 | } 447 | 448 | 449 | } 450 | 451 | fn get_archive_hashes_for_ui(winning: bool, archives: &[u64], key: &u64) -> Vec { 452 | let mut stop_skip = false; 453 | let mut final_names = vec![]; 454 | 455 | let archives = if winning { 456 | archives.iter().rev().collect::>() 457 | } else { 458 | archives.iter().collect::>() 459 | }; 460 | 461 | for archive_hash in archives { 462 | if archive_hash == key { 463 | stop_skip = true; 464 | continue; 465 | } 466 | 467 | if stop_skip { 468 | final_names.push(*archive_hash); 469 | } 470 | } 471 | 472 | if !winning { 473 | final_names.reverse(); 474 | } 475 | final_names 476 | } 477 | 478 | fn show_inline( 479 | ui: &mut egui::Ui, 480 | label_text: String, 481 | h: &u64, 482 | key: &u64, 483 | conflicts: &HashMap>, 484 | archive_map: &HashMap, 485 | winning: bool 486 | ) { 487 | ui.horizontal(|ui| { 488 | let color = if winning { 489 | Color32::GREEN 490 | } else { 491 | Color32::RED 492 | }; 493 | ui.colored_label(color, label_text); 494 | // get archive names 495 | if let Some(archives) = conflicts.get(h) { 496 | for archive_hash in get_archive_hashes_for_ui(winning, archives, key) { 497 | let archive_name = if let Some(archive_vm) = archive_map.get(&archive_hash) { 498 | archive_vm.file_name.to_owned() 499 | } else { 500 | archive_hash.to_string() 501 | }; 502 | ui.label(archive_name); 503 | } 504 | 505 | } 506 | }); 507 | } 508 | 509 | 510 | 511 | fn show_tooltip( 512 | ui: &mut egui::Ui, 513 | label_text: String, 514 | h: &u64, 515 | key: &u64, 516 | conflicts: &HashMap>, 517 | archive_map: &HashMap, 518 | winning: bool 519 | ) { 520 | let color = if winning { 521 | Color32::GREEN 522 | } else { 523 | Color32::RED 524 | }; 525 | let r = ui.colored_label(color, label_text); 526 | r.on_hover_ui(|ui| { 527 | // get archive names 528 | if let Some(archives) = conflicts.get(h) { 529 | for archive_hash in get_archive_hashes_for_ui(winning, archives, key) { 530 | 531 | let archive_name = if let Some(archive_vm) = archive_map.get(&archive_hash) { 532 | archive_vm.file_name.to_owned() 533 | } else { 534 | archive_hash.to_string() 535 | }; 536 | ui.label(archive_name); 537 | } 538 | } 539 | }); 540 | } 541 | 542 | fn show_dropdown_filelist( 543 | ui: &mut egui::Ui, 544 | label_text: String, 545 | h: &u64, 546 | key: &u64, 547 | conflicts: &HashMap>, 548 | archive_map: &HashMap, 549 | winning: bool 550 | ) { 551 | let color = if winning { 552 | Color32::GREEN 553 | } else { 554 | Color32::RED 555 | }; 556 | ui.collapsing(egui::RichText::new(label_text).color(color), |ui| { 557 | // get archive names 558 | if let Some(archives) = conflicts.get(h) { 559 | for archive_hash in archives { 560 | if archive_hash == key { 561 | continue; 562 | } 563 | 564 | let archive_name = if let Some(archive_vm) = archive_map.get(archive_hash) { 565 | archive_vm.file_name.to_owned() 566 | } else { 567 | archive_hash.to_string() 568 | }; 569 | //ui.separator(); 570 | ui.label(archive_name); 571 | } 572 | } 573 | }); 574 | } 575 | -------------------------------------------------------------------------------- /red4-conflicts/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, rust_2018_idioms)] 2 | 3 | use log::error; 4 | use red4lib::fnv1a64_hash_path; 5 | use std::fs::{self, File}; 6 | use std::io::{self, BufRead, BufReader, Write}; 7 | use std::path::Path; 8 | use std::{collections::HashMap, path::PathBuf}; 9 | 10 | mod app; 11 | 12 | const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION"); 13 | const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); 14 | 15 | #[derive(Clone)] 16 | struct ArchiveViewModel { 17 | pub file_name: String, 18 | /// winning file hashes 19 | pub wins: Vec, 20 | /// losing file hashes 21 | pub loses: Vec, 22 | /// all file hashes 23 | pub files: Vec, 24 | } 25 | 26 | impl ArchiveViewModel { 27 | pub fn get_no_conflicts(&self) -> Vec { 28 | let result: Vec = self 29 | .files 30 | .iter() 31 | .filter(|&x| !self.wins.contains(x)) 32 | .filter(|&x| !self.loses.contains(x)) 33 | .cloned() 34 | .collect(); 35 | result 36 | } 37 | } 38 | 39 | #[derive(Default, serde::Deserialize, serde::Serialize, Debug, PartialEq)] 40 | enum ETooltipVisuals { 41 | Tooltip, 42 | Inline, 43 | #[default] 44 | Collapsing, 45 | } 46 | 47 | /// We derive Deserialize/Serialize so we can persist app state on shutdown. 48 | #[derive(Default, serde::Deserialize, serde::Serialize)] 49 | #[serde(default)] // if we add new fields, give them default values when deserializing old state 50 | pub struct TemplateApp { 51 | game_path: PathBuf, 52 | // UI 53 | show_no_conflicts: bool, 54 | /// the way conflicts are disaplyed in the conflicts view 55 | tooltips_visuals: ETooltipVisuals, 56 | 57 | /// enables load order management via modlist.txt 58 | enable_modlist: bool, 59 | 60 | /// hash DB 61 | #[serde(skip)] 62 | hashes: HashMap, 63 | /// archive name lookup 64 | #[serde(skip)] 65 | archives: HashMap, 66 | /// map of file hashes to archive hashes 67 | #[serde(skip)] 68 | conflicts: HashMap>, 69 | /// archive hash load order 70 | #[serde(skip)] 71 | load_order: Vec, 72 | #[serde(skip)] 73 | last_load_order: Option>, 74 | 75 | // UI filters 76 | #[serde(skip)] 77 | text_filter: String, 78 | #[serde(skip)] 79 | file_filter: String, 80 | } 81 | 82 | impl TemplateApp { 83 | /// Called once before the first frame. 84 | pub fn new(cc: &eframe::CreationContext<'_>) -> Self { 85 | // This is also where you can customize the look and feel of egui using 86 | // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. 87 | 88 | // Load previous app state (if any). 89 | // Note that you must enable the `persistence` feature for this to work. 90 | if let Some(storage) = cc.storage { 91 | return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); 92 | } 93 | 94 | Default::default() 95 | } 96 | 97 | /// Returns the conflict map of this [`TemplateApp`]. Also sets archive and conflict maps 98 | fn generate_conflict_map(&mut self) { 99 | let old_archives = self.archives.clone(); 100 | self.archives.clear(); 101 | self.conflicts.clear(); 102 | 103 | let mut conflict_map: HashMap> = HashMap::default(); 104 | 105 | // scan 106 | let mut mods = self.load_order.clone(); 107 | mods.reverse(); 108 | 109 | for archive_name in mods.iter() { 110 | let archive_file_path = &self.game_path.join(archive_name); 111 | let archive_hash = fnv1a64_hash_path(archive_file_path); 112 | log::info!("parsing {}", archive_file_path.display()); 113 | 114 | // read or get the archive 115 | let mut archive_or_none: Option = None; 116 | if let Some(archive) = old_archives.get(&archive_hash) { 117 | // no need to read the file again 118 | let mut empty_vm = archive.clone(); 119 | // but clean it since we're calculating conflicts anew 120 | empty_vm.wins.clear(); 121 | empty_vm.loses.clear(); 122 | 123 | archive_or_none = Some(empty_vm); 124 | } else if let Ok(archive) = red4lib::archive::open_read(archive_file_path) { 125 | if let Some(archive_file_name) = 126 | archive_file_path.file_name().and_then(|f| f.to_str()) 127 | { 128 | // conflicts 129 | let mut hashes = archive 130 | .get_entries() 131 | .clone() 132 | .into_keys() 133 | .collect::>(); 134 | hashes.sort(); 135 | 136 | let vm = ArchiveViewModel { 137 | file_name: archive_file_name.to_owned(), 138 | files: hashes.clone(), 139 | wins: vec![], 140 | loses: vec![], 141 | }; 142 | 143 | archive_or_none = Some(vm); 144 | } 145 | } 146 | 147 | if let Some(mut archive_vm) = archive_or_none { 148 | for hash in &archive_vm.files { 149 | if let Some(archive_names) = conflict_map.get_mut(hash) { 150 | // found a conflict 151 | // update vms 152 | // add this file to all previous archive's losing files 153 | for archive in archive_names.iter() { 154 | if !self.archives.get(archive).unwrap().loses.contains(hash) { 155 | self.archives.get_mut(archive).unwrap().loses.push(*hash); 156 | } 157 | } 158 | // add the current archive to the list of conflicting archives last 159 | if !archive_names.contains(&archive_hash) { 160 | archive_names.push(archive_hash); 161 | } 162 | // add this file to this mods winning files 163 | if !archive_vm.wins.contains(hash) { 164 | archive_vm.wins.push(*hash); 165 | } 166 | } else { 167 | // first occurance 168 | conflict_map.insert(*hash, vec![archive_hash]); 169 | } 170 | } 171 | 172 | self.archives.insert(archive_hash, archive_vm); 173 | } 174 | } 175 | 176 | // clean list 177 | let mut conflicts: HashMap> = HashMap::default(); 178 | for (hash, archives) in conflict_map.iter().filter(|p| p.1.len() > 1) { 179 | // insert 180 | conflicts.insert(*hash, archives.clone()); 181 | } 182 | 183 | //temp_load_order.reverse(); 184 | self.conflicts = conflicts; 185 | } 186 | 187 | /// Clear and regenerate load order 188 | pub fn reload_load_order(&mut self) { 189 | self.load_order.clear(); 190 | 191 | let mut mods: Vec = get_files(&self.game_path, "archive"); 192 | // load order 193 | mods.sort_by(|a, b| { 194 | a.to_string_lossy() 195 | .as_bytes() 196 | .cmp(b.to_string_lossy().as_bytes()) 197 | }); 198 | // load according to modlist.txt 199 | let mut final_order: Vec = vec![]; 200 | let modlist_name = "modlist.txt"; 201 | if let Ok(lines) = read_file_to_vec(&self.game_path.join(modlist_name)) { 202 | for name in lines { 203 | let file_name = self.game_path.join(name); 204 | if mods.contains(&file_name) { 205 | final_order.push(file_name.to_owned()); 206 | } 207 | } 208 | // add remaining mods last 209 | for m in mods { 210 | if !final_order.contains(&m) { 211 | final_order.push(m); 212 | } 213 | } 214 | } else { 215 | final_order = mods; 216 | } 217 | // TODO Redmods 218 | 219 | self.load_order = pathbuf_to_string_vec(final_order); 220 | } 221 | 222 | fn serialize_load_order(&self) { 223 | if !self.enable_modlist { 224 | return; 225 | } 226 | 227 | let modlist_name = "modlist.txt"; 228 | if let Ok(mut file) = std::fs::File::create(self.game_path.join(modlist_name)) { 229 | for line in &self.load_order { 230 | let new_line = format!("{}\r\n", line); 231 | match file.write_all(new_line.as_bytes()) { 232 | Ok(_) => {} 233 | Err(err) => { 234 | error!("failed to write line {}", err); 235 | } 236 | } 237 | } 238 | } else { 239 | error!("failed to write load order"); 240 | } 241 | } 242 | } 243 | 244 | fn read_file_to_vec(file_path: &PathBuf) -> io::Result> { 245 | let file = File::open(file_path)?; 246 | let reader = BufReader::new(file); 247 | 248 | let lines: Vec = reader.lines().map_while(Result::ok).collect(); 249 | 250 | Ok(lines) 251 | } 252 | 253 | fn pathbuf_to_string_vec(paths: Vec) -> Vec { 254 | paths 255 | .into_iter() 256 | .filter_map(|path| { 257 | path.file_name() 258 | .map(|filename| filename.to_string_lossy().into_owned()) 259 | }) 260 | .collect() 261 | } 262 | 263 | /// Get top-level files of a folder with given extension 264 | fn get_files(folder_path: &Path, extension: &str) -> Vec { 265 | let mut files = Vec::new(); 266 | if !folder_path.exists() { 267 | return files; 268 | } 269 | 270 | if let Ok(entries) = fs::read_dir(folder_path) { 271 | for entry in entries.flatten() { 272 | if let Ok(file_type) = entry.file_type() { 273 | if file_type.is_file() { 274 | if let Some(ext) = entry.path().extension() { 275 | if ext == extension { 276 | files.push(entry.path()); 277 | } 278 | } 279 | } 280 | } 281 | } 282 | } 283 | 284 | files 285 | } 286 | -------------------------------------------------------------------------------- /red4-conflicts/src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, rust_2018_idioms)] 2 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release 3 | 4 | const CARGO_NAME: &str = env!("CARGO_PKG_NAME"); 5 | 6 | // When compiling natively: 7 | #[cfg(not(target_arch = "wasm32"))] 8 | fn main() -> eframe::Result<()> { 9 | let _ = simple_logging::log_to_file(format!("{}.log", CARGO_NAME), log::LevelFilter::Info); 10 | 11 | let native_options = eframe::NativeOptions { 12 | viewport: egui::ViewportBuilder::default() 13 | .with_inner_size([400.0, 300.0]) 14 | .with_min_inner_size([300.0, 220.0]) 15 | .with_icon( 16 | // NOE: Adding an icon is optional 17 | eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..]) 18 | .unwrap(), 19 | ), 20 | ..Default::default() 21 | }; 22 | 23 | eframe::run_native( 24 | "Red4 Conflict Checker", 25 | native_options, 26 | Box::new(|cc| Ok(Box::new(red4_conflicts::TemplateApp::new(cc)))), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /red4-tweak-browser.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "red4-tweak-browser" 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /red4-tweak-browser/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | vms.json 3 | result.json 4 | .cargo/config.toml 5 | -------------------------------------------------------------------------------- /red4-tweak-browser/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "red4-tweak-browser" 3 | version = "0.3.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | walkdir = "2.5" 9 | serde_json = "1" 10 | serde = { version = "1.0", features = ["derive"] } 11 | egui = "0.29.0" 12 | eframe = { version = "0.29.0", default-features = false, features = [ 13 | "default_fonts", # Embed the default egui fonts. 14 | "glow", # Use the glow rendering backend. Alternative: "wgpu". 15 | "persistence", # Enable restoring app state when restarting the app. 16 | ] } 17 | log = "0.4" 18 | env_logger = "0.11" 19 | rfd = "0.15" 20 | -------------------------------------------------------------------------------- /red4-tweak-browser/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-tweak-browser/assets/favicon.ico -------------------------------------------------------------------------------- /red4-tweak-browser/assets/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-tweak-browser/assets/icon-1024.png -------------------------------------------------------------------------------- /red4-tweak-browser/assets/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-tweak-browser/assets/icon-256.png -------------------------------------------------------------------------------- /red4-tweak-browser/assets/icon_ios_touch_192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-tweak-browser/assets/icon_ios_touch_192.png -------------------------------------------------------------------------------- /red4-tweak-browser/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "egui Template PWA", 3 | "short_name": "egui-template-pwa", 4 | "icons": [ 5 | { 6 | "src": "./icon-256.png", 7 | "sizes": "256x256", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./maskable_icon_x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png", 14 | "purpose": "any maskable" 15 | }, 16 | { 17 | "src": "./icon-1024.png", 18 | "sizes": "1024x1024", 19 | "type": "image/png" 20 | } 21 | ], 22 | "lang": "en-US", 23 | "id": "/index.html", 24 | "start_url": "./index.html", 25 | "display": "standalone", 26 | "background_color": "white", 27 | "theme_color": "white" 28 | } 29 | -------------------------------------------------------------------------------- /red4-tweak-browser/assets/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rfuzzo/Cyberpunk-utility/61aa88fbb2f771e8fc695a1f99bc0ce0866f6b27/red4-tweak-browser/assets/maskable_icon_x512.png -------------------------------------------------------------------------------- /red4-tweak-browser/assets/sw.js: -------------------------------------------------------------------------------- 1 | var cacheName = 'egui-template-pwa'; 2 | var filesToCache = [ 3 | './', 4 | './index.html', 5 | './plox_gui.js', 6 | './plox_gui_bg.wasm', 7 | ]; 8 | 9 | /* Start the service worker and cache all of the app's content */ 10 | self.addEventListener('install', function (e) { 11 | e.waitUntil( 12 | caches.open(cacheName).then(function (cache) { 13 | return cache.addAll(filesToCache); 14 | }) 15 | ); 16 | }); 17 | 18 | /* Serve cached content when offline */ 19 | self.addEventListener('fetch', function (e) { 20 | e.respondWith( 21 | caches.match(e.request).then(function (response) { 22 | return response || fetch(e.request); 23 | }) 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /red4-tweak-browser/src/app.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use rfd::FileDialog; 3 | use std::{collections::HashMap, path::PathBuf}; 4 | 5 | use crate::{ 6 | get_children_recursive, get_hierarchy, get_parents, get_records, TweakRecord, TweakRecordVm, 7 | }; 8 | 9 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 10 | 11 | /// We derive Deserialize/Serialize so we can persist app state on shutdown. 12 | #[derive(serde::Deserialize, serde::Serialize)] 13 | #[serde(default)] // if we add new fields, give them default values when deserializing old state 14 | pub struct TemplateApp { 15 | // Example stuff: 16 | vms: Option>, 17 | gamepath: PathBuf, 18 | 19 | #[serde(skip)] 20 | show_setup: bool, 21 | 22 | // cache packages 23 | #[serde(skip)] 24 | packages: Vec, 25 | #[serde(skip)] 26 | filtered_packages: Vec, 27 | // regenerate token 28 | regenerate_filtered_packages: bool, 29 | // filters 30 | #[serde(skip)] 31 | filter: String, 32 | #[serde(skip)] 33 | filter_package: String, 34 | #[serde(skip)] 35 | query: String, 36 | // selected 37 | #[serde(skip)] 38 | current_record_name: String, 39 | } 40 | 41 | impl Default for TemplateApp { 42 | fn default() -> Self { 43 | Self { 44 | vms: None, 45 | packages: vec![], 46 | filtered_packages: vec![], 47 | regenerate_filtered_packages: true, 48 | show_setup: false, 49 | gamepath: PathBuf::from(""), 50 | filter: "".to_owned(), 51 | filter_package: "".to_owned(), 52 | query: "".to_owned(), 53 | current_record_name: "".to_owned(), 54 | } 55 | } 56 | } 57 | 58 | impl eframe::App for TemplateApp { 59 | /// Called by the frame work to save state before shutdown. 60 | fn save(&mut self, storage: &mut dyn eframe::Storage) { 61 | eframe::set_value(storage, eframe::APP_KEY, self); 62 | } 63 | 64 | /// Called each time the UI needs repainting, which may be many times per second. 65 | fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 66 | // Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. 67 | // For inspiration and more examples, go to https://emilk.github.io/egui 68 | 69 | // menu bar 70 | self.menu_view(ctx, _frame); 71 | 72 | let healthy = self.check_health(); 73 | if !healthy || self.show_setup { 74 | // show only game path input dialogue 75 | self.setup_view(ctx); 76 | return; 77 | } 78 | 79 | if self.packages.is_empty() { 80 | if let Some(vms) = &self.vms { 81 | self.packages = get_package_names(&vms.keys().collect::>()); 82 | } 83 | } 84 | 85 | // filter the packages if query is active 86 | if self.query.is_empty() && self.regenerate_filtered_packages { 87 | self.filtered_packages = self.packages.clone(); 88 | self.regenerate_filtered_packages = false; 89 | } 90 | 91 | // main ui 92 | self.left_panel(ctx); 93 | self.main_panel(ctx); 94 | } 95 | } 96 | 97 | impl TemplateApp { 98 | /// Called once before the first frame. 99 | pub fn new(cc: &eframe::CreationContext<'_>) -> Self { 100 | // This is also where you can customize the look and feel of egui using 101 | // `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`. 102 | 103 | // Load previous app state (if any). 104 | // Note that you must enable the `persistence` feature for this to work. 105 | if let Some(storage) = cc.storage { 106 | return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); 107 | } 108 | 109 | Default::default() 110 | } 111 | 112 | /// Check if setup was succesfull 113 | pub fn check_health(&mut self) -> bool { 114 | if !PathBuf::from(&self.gamepath).exists() { 115 | return false; 116 | } 117 | 118 | if let Some(vms) = &self.vms { 119 | return !vms.is_empty(); 120 | } 121 | 122 | false 123 | } 124 | 125 | /// Generates the viewmodels from the tweak source path 126 | pub fn first_setup(&mut self) { 127 | let path = PathBuf::from(&self.gamepath); 128 | if !PathBuf::from(&self.gamepath).exists() { 129 | return; 130 | } 131 | 132 | let records: Vec = get_records(&path); 133 | info!("Found {} records", records.len()); 134 | 135 | let vms = get_hierarchy(records); 136 | info!("Found {} vms", vms.len()); 137 | 138 | self.vms = Some(vms); 139 | } 140 | 141 | /// View for the app setup 142 | fn setup_view(&mut self, ctx: &egui::Context) { 143 | egui::CentralPanel::default().show(ctx, |ui| { 144 | // The central panel the region left after adding TopPanel's and SidePanel's 145 | ui.heading("Tweak Utils"); 146 | ui.separator(); 147 | ui.horizontal(|ui| { 148 | ui.label("Tweak folder path: "); 149 | ui.label(self.gamepath.display().to_string()); 150 | if ui.button("...").clicked() { 151 | let dir = FileDialog::new().set_directory("/").pick_folder(); 152 | if let Some(folder) = dir { 153 | self.gamepath = folder; 154 | } 155 | } 156 | }); 157 | 158 | if ui.button("Generate").clicked() { 159 | self.first_setup(); 160 | self.show_setup = false; 161 | } 162 | }); 163 | } 164 | 165 | /// View for the app menu bar 166 | fn menu_view(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 167 | egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { 168 | // The top panel is often a good place for a menu bar: 169 | egui::menu::bar(ui, |ui| { 170 | ui.menu_button("File", |ui| { 171 | if ui.button("Setup").clicked() { 172 | self.show_setup = true; 173 | ui.close_menu(); 174 | } 175 | ui.separator(); 176 | if ui.button("Quit").clicked() { 177 | ctx.send_viewport_cmd(egui::ViewportCommand::Close); 178 | } 179 | }); 180 | 181 | ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { 182 | egui::global_theme_preference_buttons(ui); 183 | ui.add_space(16.0); 184 | 185 | ui.label(format!("v{}", VERSION)); 186 | }); 187 | }); 188 | }); 189 | } 190 | 191 | /// View for the left records list panel 192 | fn left_panel(&mut self, ctx: &egui::Context) { 193 | egui::SidePanel::left("left_panel").show(ctx, |ui| { 194 | ui.heading("Tweak Records"); 195 | 196 | if let Some(vms) = &self.vms { 197 | ui.horizontal(|ui| { 198 | ui.label("Search all: "); 199 | let response = ui.text_edit_singleline(&mut self.query); 200 | if response.changed() { 201 | // text changed: set regen token 202 | self.regenerate_filtered_packages = true; 203 | } 204 | if ui.button("x").clicked() { 205 | self.query.clear(); 206 | } 207 | }); 208 | ui.separator(); 209 | 210 | ui.horizontal(|ui| { 211 | // text filter 212 | ui.label("Filter: "); 213 | ui.text_edit_singleline(&mut self.filter); 214 | if ui.button("x").clicked() { 215 | self.filter.clear(); 216 | } 217 | // package filter 218 | ui.separator(); 219 | egui::ComboBox::from_id_salt("cb_package") 220 | .wrap_mode(egui::TextWrapMode::Truncate) 221 | .selected_text(format!("{:?}", &mut self.filter_package)) 222 | .show_ui(ui, |ui| { 223 | for p in &self.filtered_packages { 224 | ui.selectable_value(&mut self.filter_package, p.clone(), p); 225 | } 226 | }); 227 | }); 228 | ui.separator(); 229 | 230 | // hierarchy view 231 | if self.query.is_empty() { 232 | let mut top_level_records = vms 233 | .iter() 234 | .filter(|p| p.1.parent.is_none()) 235 | .collect::>(); 236 | top_level_records.sort_by_key(|k| k.0); 237 | 238 | egui::ScrollArea::vertical() 239 | .auto_shrink([false; 2]) 240 | .show(ui, |ui| { 241 | egui::Grid::new("hierarchy_grid") 242 | .num_columns(1) 243 | //.striped(true) 244 | .show(ui, |ui| { 245 | for (name, vm) in top_level_records { 246 | if !self.filter.is_empty() 247 | && !name 248 | .to_lowercase() 249 | .contains(&self.filter.to_lowercase()) 250 | { 251 | continue; 252 | } 253 | 254 | add_tree_node( 255 | ui, 256 | vm, 257 | name, 258 | vms, 259 | &mut self.current_record_name, 260 | ); 261 | 262 | ui.end_row(); 263 | } 264 | }); 265 | }); 266 | } 267 | // query view 268 | else { 269 | let mut result = vms 270 | .iter() 271 | .filter(|p| p.0.to_lowercase().contains(&self.query.to_lowercase())) 272 | .collect::>(); 273 | result.sort_by_key(|k| k.0); 274 | 275 | if self.regenerate_filtered_packages { 276 | self.filtered_packages = 277 | get_package_names(&result.iter().map(|f| f.0).collect::>()); 278 | self.regenerate_filtered_packages = false; 279 | } 280 | 281 | egui::ScrollArea::vertical() 282 | .auto_shrink([false; 2]) 283 | .show(ui, |ui| { 284 | egui::Grid::new("query_grid") 285 | .num_columns(1) 286 | //.striped(true) 287 | .show(ui, |ui| { 288 | for (name, _vm) in result { 289 | // filter by package 290 | if !self.filter_package.is_empty() 291 | && !name.to_lowercase().contains(&format!( 292 | "{}.", 293 | self.filter_package.to_lowercase() 294 | )) 295 | { 296 | continue; 297 | } 298 | // filter by name 299 | if !self.filter.is_empty() 300 | && !name 301 | .to_lowercase() 302 | .contains(&self.filter.to_lowercase()) 303 | { 304 | continue; 305 | } 306 | 307 | if ui 308 | .add(egui::Label::new(name).sense(egui::Sense::click())) 309 | .clicked() 310 | { 311 | self.current_record_name = name.to_owned(); 312 | } 313 | ui.end_row(); 314 | } 315 | }); 316 | }); 317 | } 318 | } 319 | }); 320 | } 321 | 322 | /// View for the record main (details) 323 | fn main_panel(&mut self, ctx: &egui::Context) { 324 | egui::CentralPanel::default().show(ctx, |ui| { 325 | // The central panel the region left after adding TopPanel's and SidePanel's 326 | ui.heading("Details"); 327 | ui.separator(); 328 | 329 | if let Some(vms) = &self.vms { 330 | if let Some(record) = vms.get(&self.current_record_name) { 331 | // breadcrumb 332 | let parents = get_parents(vms, &self.current_record_name); 333 | 334 | egui::ScrollArea::horizontal().show(ui, |ui| { 335 | ui.horizontal(|ui| { 336 | for (i, p) in parents.iter().enumerate() { 337 | if ui.button(p).clicked() { 338 | self.current_record_name = p.to_string(); 339 | } 340 | if i < parents.len() - 1 { 341 | ui.label(">"); 342 | } 343 | } 344 | }); 345 | }); 346 | 347 | // record name 348 | ui.horizontal(|ui| { 349 | ui.label("Record: "); 350 | ui.text_edit_singleline(&mut self.current_record_name.as_str()); 351 | // parent name 352 | if let Some(parent) = &record.parent { 353 | ui.label(" : "); 354 | if ui.button(parent).clicked() { 355 | self.current_record_name = parent.to_string(); 356 | } 357 | } 358 | }); 359 | 360 | // get details 361 | ui.separator(); 362 | 363 | if record.children.is_some() { 364 | // list in ui 365 | egui::CollapsingHeader::new("Children records").show(ui, |ui| { 366 | egui::ScrollArea::vertical() 367 | .auto_shrink([false; 2]) 368 | .show(ui, |ui| { 369 | for child_name in 370 | get_children_recursive(vms, &self.current_record_name) 371 | { 372 | if let Some(_child_vm) = vms.get(child_name.as_str()) { 373 | if ui.button(&child_name).clicked() { 374 | // navigate to record 375 | self.current_record_name = child_name.to_string(); 376 | } 377 | } 378 | } 379 | }); 380 | }); 381 | 382 | // list as json 383 | egui::CollapsingHeader::new("Children records (text)").show(ui, |ui| { 384 | ui.horizontal(|ui| { 385 | if ui.button("Copy to clipboard").clicked() { 386 | let result = 387 | get_children_recursive(vms, &self.current_record_name); 388 | if let Ok(json) = serde_json::to_string_pretty(&result) { 389 | ui.output_mut(|o| o.copied_text = json); 390 | } 391 | } 392 | if ui.button("Generate tweakXL instances").clicked() { 393 | if let Some(record_name) = 394 | self.current_record_name.split('.').nth(1) 395 | { 396 | if let Some(package) = 397 | self.current_record_name.split('.').nth(0) 398 | { 399 | let mut text = 400 | format!("{}.$(name):\n $instances:\n", package); 401 | 402 | // add self 403 | text += format!(" - {{ name: {} }}\n", record_name) 404 | .as_str(); 405 | // add children 406 | let children = get_children_recursive( 407 | vms, 408 | &self.current_record_name, 409 | ); 410 | for c in children { 411 | if let Some(child_record_name) = c.split('.').nth(1) 412 | { 413 | if let Some(child_package_name) = 414 | c.split('.').nth(0) 415 | { 416 | if child_package_name == package { 417 | text += format!( 418 | " - {{ name: {} }}\n", 419 | child_record_name 420 | ) 421 | .as_str(); 422 | } 423 | } 424 | } 425 | } 426 | ui.output_mut(|o| o.copied_text = text); 427 | } 428 | } 429 | } 430 | }); 431 | 432 | egui::ScrollArea::vertical() 433 | .auto_shrink([false; 2]) 434 | .show(ui, |ui| { 435 | let result = 436 | get_children_recursive(vms, &self.current_record_name); 437 | if let Ok(json) = serde_json::to_string_pretty(&result) { 438 | //egui::Frame::none().fill(egui::Color32::DARK_GRAY).show( 439 | // ui, 440 | // |ui| { 441 | ui.add_sized( 442 | ui.available_size(), 443 | egui::TextEdit::multiline(&mut json.as_str()), 444 | ); 445 | // }, 446 | //); 447 | } 448 | }); 449 | }); 450 | } 451 | } 452 | } 453 | }); 454 | } 455 | } 456 | 457 | fn get_package_names(vms: &[&String]) -> Vec { 458 | let mut packages = vms 459 | .iter() 460 | .filter(|p| p.contains('.')) 461 | .filter_map(|f| f.split('.').next()) 462 | .map(|f| f.to_owned()) 463 | .collect::>(); 464 | packages.sort(); 465 | packages.dedup(); 466 | packages 467 | } 468 | 469 | /// Adds an expandable node to the ui tree recursively 470 | fn add_tree_node( 471 | ui: &mut egui::Ui, 472 | vm: &TweakRecordVm, 473 | name: &String, 474 | vms: &HashMap, 475 | current_record_name: &mut String, 476 | ) { 477 | if let Some(children) = &vm.children { 478 | let r = egui::CollapsingHeader::new(name).show(ui, |ui| { 479 | for child_name in children { 480 | if let Some(child_vm) = vms.get(child_name) { 481 | add_tree_node(ui, child_vm, child_name, vms, current_record_name); 482 | } 483 | } 484 | }); 485 | if r.header_response.clicked() { 486 | *current_record_name = name.to_owned(); 487 | } 488 | } else if ui 489 | .add(egui::Label::new(name).sense(egui::Sense::click())) 490 | .clicked() 491 | { 492 | // show details 493 | *current_record_name = name.to_owned(); 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /red4-tweak-browser/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all, rust_2018_idioms)] 2 | 3 | mod app; 4 | pub use app::TemplateApp; 5 | use log::info; 6 | use std::{ 7 | collections::HashMap, 8 | fs::File, 9 | io::{self, BufRead}, 10 | path::{Path, PathBuf}, 11 | }; 12 | 13 | use serde::{Deserialize, Serialize}; 14 | use walkdir::{DirEntry, WalkDir}; 15 | 16 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 17 | pub struct TweakRecord { 18 | pub name: String, 19 | pub base: Option, 20 | pub package: String, 21 | pub imports: Vec, 22 | } 23 | impl TweakRecord { 24 | fn full_name(&self) -> String { 25 | format!("{}.{}", self.package, self.name) 26 | } 27 | fn possible_base_names(&self) -> Option> { 28 | if self.base.is_none() { 29 | None 30 | } else { 31 | let mut results: Vec = vec![]; 32 | 33 | // possible is in packaege 34 | results.push(format!( 35 | "{}.{}", 36 | self.package, 37 | self.base.to_owned().unwrap() 38 | )); 39 | 40 | // and all imports 41 | for i in &self.imports { 42 | let n = format!("{}.{}", i, self.base.to_owned().unwrap()); 43 | results.push(n); 44 | } 45 | 46 | Some(results) 47 | } 48 | } 49 | } 50 | 51 | #[derive(Serialize, Deserialize, Debug, PartialEq)] 52 | pub struct TweakRecordVm { 53 | //pub full_name: String, 54 | pub children: Option>, 55 | pub parent: Option, 56 | } 57 | 58 | // The output is wrapped in a Result to allow matching on errors 59 | // Returns an Iterator to the Reader of the lines of the file. 60 | fn read_lines

(filename: P) -> io::Result>> 61 | where 62 | P: AsRef, 63 | { 64 | let file = File::open(filename)?; 65 | Ok(io::BufReader::new(file).lines()) 66 | } 67 | 68 | fn get_parents(vms: &HashMap, record: &str) -> Vec { 69 | let mut result: Vec = vec![]; 70 | 71 | if let Some(b) = vms.get(record) { 72 | result.push(record.to_owned()); 73 | if let Some(base) = &b.parent { 74 | let inner = get_parents(vms, base.as_str()); 75 | for i in inner { 76 | result.push(i); 77 | } 78 | } 79 | } 80 | result 81 | } 82 | 83 | fn get_children_recursive(vms: &HashMap, record: &String) -> Vec { 84 | let mut result: Vec = vec![]; 85 | 86 | if let Some(b) = vms.get(record) { 87 | if let Some(children) = &b.children { 88 | for child in children.iter() { 89 | result.push(child.to_owned()); 90 | 91 | let inner_result = get_children_recursive(vms, child); 92 | for inner_child in inner_result { 93 | result.push(inner_child); 94 | } 95 | } 96 | } 97 | } 98 | 99 | result.sort(); 100 | result.dedup(); 101 | result 102 | } 103 | 104 | pub fn get_hierarchy(records: Vec) -> HashMap { 105 | let mut vms: HashMap = HashMap::default(); 106 | 107 | // add all records once 108 | for r in records.iter() { 109 | let v = TweakRecordVm { 110 | children: None, 111 | parent: if r.base.is_some() { 112 | Some("dummy".to_owned()) 113 | } else { 114 | None 115 | }, 116 | }; 117 | vms.insert(r.full_name(), v); 118 | } 119 | 120 | // populate dependent data 121 | for r in records.iter().filter(|f| f.base.is_some()) { 122 | // fill in parent 123 | let mut parent_name: Option = None; 124 | if let Some(base_names) = r.possible_base_names() { 125 | for base_name in &base_names { 126 | // try getting a real record 127 | if let Some(base) = vms.get_mut(base_name) { 128 | // a base was found 129 | // get children 130 | if let Some(children) = &mut base.children { 131 | children.push(r.full_name()); 132 | } else { 133 | base.children = Some(vec![r.full_name()]); 134 | } 135 | parent_name = Some(base_name.to_string()); 136 | break; 137 | } 138 | } 139 | } 140 | 141 | // save parent in vm 142 | if let Some(this_vm) = vms.get_mut(&r.full_name()) { 143 | this_vm.parent = parent_name.clone(); 144 | } 145 | } 146 | 147 | vms 148 | } 149 | 150 | fn is_hidden(entry: &DirEntry) -> bool { 151 | entry 152 | .file_name() 153 | .to_str() 154 | .map(|s| s.starts_with('.')) 155 | .unwrap_or(false) 156 | } 157 | 158 | pub fn get_records(path: &PathBuf) -> Vec { 159 | let mut records: Vec = vec![]; 160 | let mut parsed = 0; 161 | let mut total = 0; 162 | 163 | for entry in WalkDir::new(path) 164 | .into_iter() 165 | .filter_entry(|e| !is_hidden(e)) 166 | .filter_map(|e| e.ok()) 167 | { 168 | let filename = entry.path(); 169 | if filename.is_dir() { 170 | continue; 171 | } 172 | if let Some(ext) = filename.extension() { 173 | if !ext.to_string_lossy().eq("tweak") { 174 | continue; 175 | } 176 | } 177 | 178 | // parse each file 179 | total += 1; 180 | let mut ignore = false; 181 | let mut package = "".to_owned(); 182 | let mut imports: Vec = vec![]; 183 | 184 | if let Ok(lines) = read_lines(entry.path()) { 185 | parsed += 1; 186 | for line in lines.map_while(Result::ok) { 187 | if line.starts_with("package ") { 188 | package = line["package".len() + 1..].to_string(); 189 | 190 | continue; 191 | } 192 | if line.starts_with("using ") { 193 | let usings = line["using".len() + 1..].to_string(); 194 | let u_splits = usings.split(", ").collect::>(); 195 | imports = u_splits 196 | .into_iter() 197 | .map(|f| f.to_owned()) 198 | .collect::>(); 199 | continue; 200 | } 201 | if line.contains('=') { 202 | continue; 203 | } 204 | if line.starts_with("[ ") && line.contains(']') { 205 | continue; 206 | } 207 | if line.starts_with('[') && !line.contains(']') { 208 | ignore = true; 209 | continue; 210 | } 211 | if line.is_empty() { 212 | continue; 213 | } 214 | // props 215 | if line.starts_with('{') { 216 | ignore = true; 217 | continue; 218 | } 219 | if line.starts_with('}') { 220 | ignore = false; 221 | continue; 222 | } 223 | if line.starts_with(']') { 224 | ignore = false; 225 | continue; 226 | } 227 | if ignore { 228 | continue; 229 | } 230 | 231 | let mut name = line.to_owned(); 232 | let mut base = None; 233 | if name.contains(" : ") { 234 | let splits = line.split(" : ").collect::>(); 235 | name = splits.first().unwrap().to_string(); 236 | base = Some(splits.last().unwrap().to_string()); 237 | } 238 | 239 | let record: TweakRecord = TweakRecord { 240 | name, 241 | base, 242 | package: package.to_owned(), 243 | imports: imports.to_owned(), 244 | }; 245 | if !records.contains(&record) { 246 | records.push(record); 247 | //println!("Adding record {}", &line); 248 | } 249 | } 250 | 251 | info!("Parsed {} files", parsed); 252 | } 253 | } 254 | 255 | info!("Parsed {}/{} files", parsed, total); 256 | records 257 | } 258 | -------------------------------------------------------------------------------- /red4-tweak-browser/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() -> eframe::Result<()> { 2 | env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). 3 | 4 | let native_options = eframe::NativeOptions { 5 | viewport: egui::ViewportBuilder::default() 6 | .with_inner_size([400.0, 300.0]) 7 | .with_min_inner_size([300.0, 220.0]) 8 | .with_icon( 9 | // NOE: Adding an icon is optional 10 | eframe::icon_data::from_png_bytes(&include_bytes!("../assets/icon-256.png")[..]) 11 | .unwrap(), 12 | ), 13 | ..Default::default() 14 | }; 15 | 16 | eframe::run_native( 17 | "Tweak Utils", 18 | native_options, 19 | Box::new(|cc| Ok(Box::new(red4_tweak_browser::TemplateApp::new(cc)))), 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /red4-update.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "red4-update" 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /red4-update/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode/launch.json 3 | -------------------------------------------------------------------------------- /red4-update/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "adler2" 7 | version = "2.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 10 | 11 | [[package]] 12 | name = "aes" 13 | version = "0.8.4" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" 16 | dependencies = [ 17 | "cfg-if", 18 | "cipher", 19 | "cpufeatures", 20 | ] 21 | 22 | [[package]] 23 | name = "anstream" 24 | version = "0.6.15" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 27 | dependencies = [ 28 | "anstyle", 29 | "anstyle-parse", 30 | "anstyle-query", 31 | "anstyle-wincon", 32 | "colorchoice", 33 | "is_terminal_polyfill", 34 | "utf8parse", 35 | ] 36 | 37 | [[package]] 38 | name = "anstyle" 39 | version = "1.0.8" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 42 | 43 | [[package]] 44 | name = "anstyle-parse" 45 | version = "0.2.5" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 48 | dependencies = [ 49 | "utf8parse", 50 | ] 51 | 52 | [[package]] 53 | name = "anstyle-query" 54 | version = "1.1.1" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 57 | dependencies = [ 58 | "windows-sys 0.52.0", 59 | ] 60 | 61 | [[package]] 62 | name = "anstyle-wincon" 63 | version = "3.0.4" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 66 | dependencies = [ 67 | "anstyle", 68 | "windows-sys 0.52.0", 69 | ] 70 | 71 | [[package]] 72 | name = "arbitrary" 73 | version = "1.3.2" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" 76 | dependencies = [ 77 | "derive_arbitrary", 78 | ] 79 | 80 | [[package]] 81 | name = "block-buffer" 82 | version = "0.10.4" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 85 | dependencies = [ 86 | "generic-array", 87 | ] 88 | 89 | [[package]] 90 | name = "bumpalo" 91 | version = "3.16.0" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 94 | 95 | [[package]] 96 | name = "byteorder" 97 | version = "1.5.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 100 | 101 | [[package]] 102 | name = "bzip2" 103 | version = "0.4.4" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" 106 | dependencies = [ 107 | "bzip2-sys", 108 | "libc", 109 | ] 110 | 111 | [[package]] 112 | name = "bzip2-sys" 113 | version = "0.1.11+1.0.8" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 116 | dependencies = [ 117 | "cc", 118 | "libc", 119 | "pkg-config", 120 | ] 121 | 122 | [[package]] 123 | name = "cc" 124 | version = "1.1.28" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" 127 | dependencies = [ 128 | "jobserver", 129 | "libc", 130 | "shlex", 131 | ] 132 | 133 | [[package]] 134 | name = "cfg-if" 135 | version = "1.0.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 138 | 139 | [[package]] 140 | name = "cipher" 141 | version = "0.4.4" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 144 | dependencies = [ 145 | "crypto-common", 146 | "inout", 147 | ] 148 | 149 | [[package]] 150 | name = "clap" 151 | version = "4.5.20" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" 154 | dependencies = [ 155 | "clap_builder", 156 | "clap_derive", 157 | ] 158 | 159 | [[package]] 160 | name = "clap_builder" 161 | version = "4.5.20" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" 164 | dependencies = [ 165 | "anstream", 166 | "anstyle", 167 | "clap_lex", 168 | "strsim", 169 | ] 170 | 171 | [[package]] 172 | name = "clap_derive" 173 | version = "4.5.18" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" 176 | dependencies = [ 177 | "heck", 178 | "proc-macro2", 179 | "quote", 180 | "syn", 181 | ] 182 | 183 | [[package]] 184 | name = "clap_lex" 185 | version = "0.7.2" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 188 | 189 | [[package]] 190 | name = "cmake" 191 | version = "0.1.51" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" 194 | dependencies = [ 195 | "cc", 196 | ] 197 | 198 | [[package]] 199 | name = "colorchoice" 200 | version = "1.0.2" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 203 | 204 | [[package]] 205 | name = "colored" 206 | version = "2.1.0" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" 209 | dependencies = [ 210 | "lazy_static", 211 | "windows-sys 0.48.0", 212 | ] 213 | 214 | [[package]] 215 | name = "constant_time_eq" 216 | version = "0.3.1" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" 219 | 220 | [[package]] 221 | name = "cpufeatures" 222 | version = "0.2.14" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" 225 | dependencies = [ 226 | "libc", 227 | ] 228 | 229 | [[package]] 230 | name = "crc" 231 | version = "3.2.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" 234 | dependencies = [ 235 | "crc-catalog", 236 | ] 237 | 238 | [[package]] 239 | name = "crc-catalog" 240 | version = "2.4.0" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" 243 | 244 | [[package]] 245 | name = "crc32fast" 246 | version = "1.4.2" 247 | source = "registry+https://github.com/rust-lang/crates.io-index" 248 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 249 | dependencies = [ 250 | "cfg-if", 251 | ] 252 | 253 | [[package]] 254 | name = "crc64" 255 | version = "2.0.0" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "2707e3afba5e19b75d582d88bc79237418f2a2a2d673d01cf9b03633b46e98f3" 258 | 259 | [[package]] 260 | name = "crossbeam-utils" 261 | version = "0.8.20" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 264 | 265 | [[package]] 266 | name = "crypto-common" 267 | version = "0.1.6" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 270 | dependencies = [ 271 | "generic-array", 272 | "typenum", 273 | ] 274 | 275 | [[package]] 276 | name = "deflate64" 277 | version = "0.1.9" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" 280 | 281 | [[package]] 282 | name = "deranged" 283 | version = "0.3.11" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 286 | dependencies = [ 287 | "powerfmt", 288 | ] 289 | 290 | [[package]] 291 | name = "derive_arbitrary" 292 | version = "1.3.2" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" 295 | dependencies = [ 296 | "proc-macro2", 297 | "quote", 298 | "syn", 299 | ] 300 | 301 | [[package]] 302 | name = "digest" 303 | version = "0.10.7" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 306 | dependencies = [ 307 | "block-buffer", 308 | "crypto-common", 309 | "subtle", 310 | ] 311 | 312 | [[package]] 313 | name = "displaydoc" 314 | version = "0.2.5" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 317 | dependencies = [ 318 | "proc-macro2", 319 | "quote", 320 | "syn", 321 | ] 322 | 323 | [[package]] 324 | name = "equivalent" 325 | version = "1.0.1" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 328 | 329 | [[package]] 330 | name = "flate2" 331 | version = "1.0.34" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" 334 | dependencies = [ 335 | "crc32fast", 336 | "miniz_oxide", 337 | ] 338 | 339 | [[package]] 340 | name = "fnv" 341 | version = "1.0.7" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 344 | 345 | [[package]] 346 | name = "generic-array" 347 | version = "0.14.7" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 350 | dependencies = [ 351 | "typenum", 352 | "version_check", 353 | ] 354 | 355 | [[package]] 356 | name = "getrandom" 357 | version = "0.2.15" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 360 | dependencies = [ 361 | "cfg-if", 362 | "libc", 363 | "wasi", 364 | ] 365 | 366 | [[package]] 367 | name = "hashbrown" 368 | version = "0.15.0" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" 371 | 372 | [[package]] 373 | name = "heck" 374 | version = "0.5.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 377 | 378 | [[package]] 379 | name = "hmac" 380 | version = "0.12.1" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 383 | dependencies = [ 384 | "digest", 385 | ] 386 | 387 | [[package]] 388 | name = "indexmap" 389 | version = "2.6.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" 392 | dependencies = [ 393 | "equivalent", 394 | "hashbrown", 395 | ] 396 | 397 | [[package]] 398 | name = "inout" 399 | version = "0.1.3" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 402 | dependencies = [ 403 | "generic-array", 404 | ] 405 | 406 | [[package]] 407 | name = "is_terminal_polyfill" 408 | version = "1.70.1" 409 | source = "registry+https://github.com/rust-lang/crates.io-index" 410 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 411 | 412 | [[package]] 413 | name = "itoa" 414 | version = "1.0.11" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 417 | 418 | [[package]] 419 | name = "jobserver" 420 | version = "0.1.32" 421 | source = "registry+https://github.com/rust-lang/crates.io-index" 422 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 423 | dependencies = [ 424 | "libc", 425 | ] 426 | 427 | [[package]] 428 | name = "lazy_static" 429 | version = "1.5.0" 430 | source = "registry+https://github.com/rust-lang/crates.io-index" 431 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 432 | 433 | [[package]] 434 | name = "libc" 435 | version = "0.2.159" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" 438 | 439 | [[package]] 440 | name = "lockfree-object-pool" 441 | version = "0.1.6" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" 444 | 445 | [[package]] 446 | name = "log" 447 | version = "0.4.22" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 450 | 451 | [[package]] 452 | name = "lzma-rs" 453 | version = "0.3.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" 456 | dependencies = [ 457 | "byteorder", 458 | "crc", 459 | ] 460 | 461 | [[package]] 462 | name = "memchr" 463 | version = "2.7.4" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 466 | 467 | [[package]] 468 | name = "miniz_oxide" 469 | version = "0.8.0" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" 472 | dependencies = [ 473 | "adler2", 474 | ] 475 | 476 | [[package]] 477 | name = "num-conv" 478 | version = "0.1.0" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 481 | 482 | [[package]] 483 | name = "num_threads" 484 | version = "0.1.7" 485 | source = "registry+https://github.com/rust-lang/crates.io-index" 486 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 487 | dependencies = [ 488 | "libc", 489 | ] 490 | 491 | [[package]] 492 | name = "once_cell" 493 | version = "1.20.2" 494 | source = "registry+https://github.com/rust-lang/crates.io-index" 495 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 496 | 497 | [[package]] 498 | name = "pbkdf2" 499 | version = "0.12.2" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 502 | dependencies = [ 503 | "digest", 504 | "hmac", 505 | ] 506 | 507 | [[package]] 508 | name = "pkg-config" 509 | version = "0.3.31" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 512 | 513 | [[package]] 514 | name = "powerfmt" 515 | version = "0.2.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 518 | 519 | [[package]] 520 | name = "ppv-lite86" 521 | version = "0.2.20" 522 | source = "registry+https://github.com/rust-lang/crates.io-index" 523 | checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" 524 | dependencies = [ 525 | "zerocopy", 526 | ] 527 | 528 | [[package]] 529 | name = "proc-macro2" 530 | version = "1.0.87" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" 533 | dependencies = [ 534 | "unicode-ident", 535 | ] 536 | 537 | [[package]] 538 | name = "quote" 539 | version = "1.0.37" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 542 | dependencies = [ 543 | "proc-macro2", 544 | ] 545 | 546 | [[package]] 547 | name = "rand" 548 | version = "0.8.5" 549 | source = "registry+https://github.com/rust-lang/crates.io-index" 550 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 551 | dependencies = [ 552 | "libc", 553 | "rand_chacha", 554 | "rand_core", 555 | ] 556 | 557 | [[package]] 558 | name = "rand_chacha" 559 | version = "0.3.1" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 562 | dependencies = [ 563 | "ppv-lite86", 564 | "rand_core", 565 | ] 566 | 567 | [[package]] 568 | name = "rand_core" 569 | version = "0.6.4" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 572 | dependencies = [ 573 | "getrandom", 574 | ] 575 | 576 | [[package]] 577 | name = "red4-update" 578 | version = "0.1.0" 579 | dependencies = [ 580 | "clap", 581 | "colored", 582 | "log", 583 | "red4lib", 584 | "serde", 585 | "serde_json", 586 | "simple_logger", 587 | ] 588 | 589 | [[package]] 590 | name = "red4lib" 591 | version = "0.2.0" 592 | dependencies = [ 593 | "byteorder", 594 | "cmake", 595 | "crc64", 596 | "fnv", 597 | "sha1", 598 | "strum", 599 | "strum_macros", 600 | "walkdir", 601 | "zip", 602 | ] 603 | 604 | [[package]] 605 | name = "rustversion" 606 | version = "1.0.17" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" 609 | 610 | [[package]] 611 | name = "ryu" 612 | version = "1.0.18" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 615 | 616 | [[package]] 617 | name = "same-file" 618 | version = "1.0.6" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 621 | dependencies = [ 622 | "winapi-util", 623 | ] 624 | 625 | [[package]] 626 | name = "serde" 627 | version = "1.0.210" 628 | source = "registry+https://github.com/rust-lang/crates.io-index" 629 | checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" 630 | dependencies = [ 631 | "serde_derive", 632 | ] 633 | 634 | [[package]] 635 | name = "serde_derive" 636 | version = "1.0.210" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" 639 | dependencies = [ 640 | "proc-macro2", 641 | "quote", 642 | "syn", 643 | ] 644 | 645 | [[package]] 646 | name = "serde_json" 647 | version = "1.0.128" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" 650 | dependencies = [ 651 | "itoa", 652 | "memchr", 653 | "ryu", 654 | "serde", 655 | ] 656 | 657 | [[package]] 658 | name = "sha1" 659 | version = "0.10.6" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 662 | dependencies = [ 663 | "cfg-if", 664 | "cpufeatures", 665 | "digest", 666 | ] 667 | 668 | [[package]] 669 | name = "shlex" 670 | version = "1.3.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 673 | 674 | [[package]] 675 | name = "simd-adler32" 676 | version = "0.3.7" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 679 | 680 | [[package]] 681 | name = "simple_logger" 682 | version = "5.0.0" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" 685 | dependencies = [ 686 | "colored", 687 | "log", 688 | "time", 689 | "windows-sys 0.48.0", 690 | ] 691 | 692 | [[package]] 693 | name = "strsim" 694 | version = "0.11.1" 695 | source = "registry+https://github.com/rust-lang/crates.io-index" 696 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 697 | 698 | [[package]] 699 | name = "strum" 700 | version = "0.26.3" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" 703 | 704 | [[package]] 705 | name = "strum_macros" 706 | version = "0.26.4" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" 709 | dependencies = [ 710 | "heck", 711 | "proc-macro2", 712 | "quote", 713 | "rustversion", 714 | "syn", 715 | ] 716 | 717 | [[package]] 718 | name = "subtle" 719 | version = "2.6.1" 720 | source = "registry+https://github.com/rust-lang/crates.io-index" 721 | checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" 722 | 723 | [[package]] 724 | name = "syn" 725 | version = "2.0.79" 726 | source = "registry+https://github.com/rust-lang/crates.io-index" 727 | checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" 728 | dependencies = [ 729 | "proc-macro2", 730 | "quote", 731 | "unicode-ident", 732 | ] 733 | 734 | [[package]] 735 | name = "thiserror" 736 | version = "1.0.64" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" 739 | dependencies = [ 740 | "thiserror-impl", 741 | ] 742 | 743 | [[package]] 744 | name = "thiserror-impl" 745 | version = "1.0.64" 746 | source = "registry+https://github.com/rust-lang/crates.io-index" 747 | checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" 748 | dependencies = [ 749 | "proc-macro2", 750 | "quote", 751 | "syn", 752 | ] 753 | 754 | [[package]] 755 | name = "time" 756 | version = "0.3.36" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 759 | dependencies = [ 760 | "deranged", 761 | "itoa", 762 | "libc", 763 | "num-conv", 764 | "num_threads", 765 | "powerfmt", 766 | "serde", 767 | "time-core", 768 | "time-macros", 769 | ] 770 | 771 | [[package]] 772 | name = "time-core" 773 | version = "0.1.2" 774 | source = "registry+https://github.com/rust-lang/crates.io-index" 775 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 776 | 777 | [[package]] 778 | name = "time-macros" 779 | version = "0.2.18" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 782 | dependencies = [ 783 | "num-conv", 784 | "time-core", 785 | ] 786 | 787 | [[package]] 788 | name = "typenum" 789 | version = "1.17.0" 790 | source = "registry+https://github.com/rust-lang/crates.io-index" 791 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 792 | 793 | [[package]] 794 | name = "unicode-ident" 795 | version = "1.0.13" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" 798 | 799 | [[package]] 800 | name = "utf8parse" 801 | version = "0.2.2" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 804 | 805 | [[package]] 806 | name = "version_check" 807 | version = "0.9.5" 808 | source = "registry+https://github.com/rust-lang/crates.io-index" 809 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 810 | 811 | [[package]] 812 | name = "walkdir" 813 | version = "2.5.0" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 816 | dependencies = [ 817 | "same-file", 818 | "winapi-util", 819 | ] 820 | 821 | [[package]] 822 | name = "wasi" 823 | version = "0.11.0+wasi-snapshot-preview1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 826 | 827 | [[package]] 828 | name = "winapi-util" 829 | version = "0.1.9" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 832 | dependencies = [ 833 | "windows-sys 0.59.0", 834 | ] 835 | 836 | [[package]] 837 | name = "windows-sys" 838 | version = "0.48.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 841 | dependencies = [ 842 | "windows-targets 0.48.5", 843 | ] 844 | 845 | [[package]] 846 | name = "windows-sys" 847 | version = "0.52.0" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 850 | dependencies = [ 851 | "windows-targets 0.52.6", 852 | ] 853 | 854 | [[package]] 855 | name = "windows-sys" 856 | version = "0.59.0" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 859 | dependencies = [ 860 | "windows-targets 0.52.6", 861 | ] 862 | 863 | [[package]] 864 | name = "windows-targets" 865 | version = "0.48.5" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 868 | dependencies = [ 869 | "windows_aarch64_gnullvm 0.48.5", 870 | "windows_aarch64_msvc 0.48.5", 871 | "windows_i686_gnu 0.48.5", 872 | "windows_i686_msvc 0.48.5", 873 | "windows_x86_64_gnu 0.48.5", 874 | "windows_x86_64_gnullvm 0.48.5", 875 | "windows_x86_64_msvc 0.48.5", 876 | ] 877 | 878 | [[package]] 879 | name = "windows-targets" 880 | version = "0.52.6" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 883 | dependencies = [ 884 | "windows_aarch64_gnullvm 0.52.6", 885 | "windows_aarch64_msvc 0.52.6", 886 | "windows_i686_gnu 0.52.6", 887 | "windows_i686_gnullvm", 888 | "windows_i686_msvc 0.52.6", 889 | "windows_x86_64_gnu 0.52.6", 890 | "windows_x86_64_gnullvm 0.52.6", 891 | "windows_x86_64_msvc 0.52.6", 892 | ] 893 | 894 | [[package]] 895 | name = "windows_aarch64_gnullvm" 896 | version = "0.48.5" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 899 | 900 | [[package]] 901 | name = "windows_aarch64_gnullvm" 902 | version = "0.52.6" 903 | source = "registry+https://github.com/rust-lang/crates.io-index" 904 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 905 | 906 | [[package]] 907 | name = "windows_aarch64_msvc" 908 | version = "0.48.5" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 911 | 912 | [[package]] 913 | name = "windows_aarch64_msvc" 914 | version = "0.52.6" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 917 | 918 | [[package]] 919 | name = "windows_i686_gnu" 920 | version = "0.48.5" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 923 | 924 | [[package]] 925 | name = "windows_i686_gnu" 926 | version = "0.52.6" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 929 | 930 | [[package]] 931 | name = "windows_i686_gnullvm" 932 | version = "0.52.6" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 935 | 936 | [[package]] 937 | name = "windows_i686_msvc" 938 | version = "0.48.5" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 941 | 942 | [[package]] 943 | name = "windows_i686_msvc" 944 | version = "0.52.6" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 947 | 948 | [[package]] 949 | name = "windows_x86_64_gnu" 950 | version = "0.48.5" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 953 | 954 | [[package]] 955 | name = "windows_x86_64_gnu" 956 | version = "0.52.6" 957 | source = "registry+https://github.com/rust-lang/crates.io-index" 958 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 959 | 960 | [[package]] 961 | name = "windows_x86_64_gnullvm" 962 | version = "0.48.5" 963 | source = "registry+https://github.com/rust-lang/crates.io-index" 964 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 965 | 966 | [[package]] 967 | name = "windows_x86_64_gnullvm" 968 | version = "0.52.6" 969 | source = "registry+https://github.com/rust-lang/crates.io-index" 970 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 971 | 972 | [[package]] 973 | name = "windows_x86_64_msvc" 974 | version = "0.48.5" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 977 | 978 | [[package]] 979 | name = "windows_x86_64_msvc" 980 | version = "0.52.6" 981 | source = "registry+https://github.com/rust-lang/crates.io-index" 982 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 983 | 984 | [[package]] 985 | name = "zerocopy" 986 | version = "0.7.35" 987 | source = "registry+https://github.com/rust-lang/crates.io-index" 988 | checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" 989 | dependencies = [ 990 | "byteorder", 991 | "zerocopy-derive", 992 | ] 993 | 994 | [[package]] 995 | name = "zerocopy-derive" 996 | version = "0.7.35" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" 999 | dependencies = [ 1000 | "proc-macro2", 1001 | "quote", 1002 | "syn", 1003 | ] 1004 | 1005 | [[package]] 1006 | name = "zeroize" 1007 | version = "1.8.1" 1008 | source = "registry+https://github.com/rust-lang/crates.io-index" 1009 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 1010 | dependencies = [ 1011 | "zeroize_derive", 1012 | ] 1013 | 1014 | [[package]] 1015 | name = "zeroize_derive" 1016 | version = "1.4.2" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 1019 | dependencies = [ 1020 | "proc-macro2", 1021 | "quote", 1022 | "syn", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "zip" 1027 | version = "2.2.0" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" 1030 | dependencies = [ 1031 | "aes", 1032 | "arbitrary", 1033 | "bzip2", 1034 | "constant_time_eq", 1035 | "crc32fast", 1036 | "crossbeam-utils", 1037 | "deflate64", 1038 | "displaydoc", 1039 | "flate2", 1040 | "hmac", 1041 | "indexmap", 1042 | "lzma-rs", 1043 | "memchr", 1044 | "pbkdf2", 1045 | "rand", 1046 | "sha1", 1047 | "thiserror", 1048 | "time", 1049 | "zeroize", 1050 | "zopfli", 1051 | "zstd", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "zopfli" 1056 | version = "0.8.1" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" 1059 | dependencies = [ 1060 | "bumpalo", 1061 | "crc32fast", 1062 | "lockfree-object-pool", 1063 | "log", 1064 | "once_cell", 1065 | "simd-adler32", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "zstd" 1070 | version = "0.13.2" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" 1073 | dependencies = [ 1074 | "zstd-safe", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "zstd-safe" 1079 | version = "7.2.1" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" 1082 | dependencies = [ 1083 | "zstd-sys", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "zstd-sys" 1088 | version = "2.0.13+zstd.1.5.6" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" 1091 | dependencies = [ 1092 | "cc", 1093 | "pkg-config", 1094 | ] 1095 | -------------------------------------------------------------------------------- /red4-update/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "red4-update" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [dependencies] 8 | log = "0.4" 9 | simple_logger = "5" 10 | clap = { version = "4.5", features = ["derive"] } 11 | colored = "2" 12 | serde = { version = "1", features = ["derive"] } 13 | serde_json = "1.0" 14 | 15 | [dependencies.red4lib] 16 | git = "https://github.com/rfuzzo/red4lib" 17 | #path = "D:\\GitHub\\__rfuzzo\\red4lib" 18 | -------------------------------------------------------------------------------- /red4-update/report.json: -------------------------------------------------------------------------------- 1 | { 2 | "test.archive": { 3 | "deleted": {}, 4 | "added": {}, 5 | "changed": { 6 | "11399646534807003211": { 7 | "hash": 11399646534807003211, 8 | "name": "base\\prefabs\\buildings\\megabuilding\\geo\\interior\\floors\\vs_apartment_floors\\vs_apartment_floor\\arch_floor_06_383094b6_mc.mesh", 9 | "archive_name": "test.archive", 10 | "sha1": "da39a3ee5e6b4bd3255bfef95601890afd879" 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /red4-update/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | #[derive(Debug, serde::Serialize, serde::Deserialize, Default)] 8 | pub struct Diff { 9 | pub deleted: HashMap, 10 | pub added: HashMap, 11 | pub changed: HashMap, 12 | } 13 | 14 | #[derive(Debug, serde::Serialize, serde::Deserialize, Default, Clone)] 15 | pub struct FileInfo { 16 | pub hash: u64, 17 | pub name: String, 18 | pub archive_name: String, 19 | pub sha1: String, 20 | } 21 | 22 | /// Get top-level files of a folder with given extension 23 | fn get_files(folder_path: &Path, extension: &str) -> Vec { 24 | let mut files = Vec::new(); 25 | if !folder_path.exists() { 26 | return files; 27 | } 28 | 29 | if let Ok(entries) = fs::read_dir(folder_path) { 30 | for entry in entries.flatten() { 31 | if let Ok(file_type) = entry.file_type() { 32 | if file_type.is_file() { 33 | if let Some(ext) = entry.path().extension() { 34 | if ext == extension { 35 | files.push(entry.path()); 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | files 44 | } 45 | 46 | pub fn get_info( 47 | archives_path: &Path, 48 | file_map: &mut HashMap, 49 | hash_map: &HashMap, 50 | ) { 51 | // TODO ignore lang option 52 | let files = get_files(archives_path, "archive"); 53 | let files_content = files.iter().filter(|f| { 54 | if let Some(file_name) = f.file_name() { 55 | return !file_name.to_string_lossy().starts_with("lang_"); 56 | } 57 | false 58 | }); 59 | for path in files_content { 60 | log::info!("Parsing {}", &path.display()); 61 | if let Ok(archive) = red4lib::archive::open_read(path) { 62 | let archive_name = path 63 | .file_name() 64 | .unwrap() 65 | .to_ascii_lowercase() 66 | .to_str() 67 | .unwrap() 68 | .to_owned(); 69 | 70 | for (hash, entry) in archive.get_entries() { 71 | let mut name = hash.to_string(); 72 | if let Some(resolved_name) = hash_map.get(hash) { 73 | name = resolved_name.to_owned(); 74 | } 75 | 76 | let mut sha = "".to_owned(); 77 | for d in entry.entry.sha1_hash() { 78 | sha += format!("{:x}", d).as_str(); 79 | } 80 | 81 | let entry = FileInfo { 82 | hash: *hash, 83 | name, 84 | archive_name: archive_name.to_owned(), 85 | sha1: sha, 86 | }; 87 | file_map.insert(*hash, entry); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /red4-update/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | env, 4 | fs::{self, File}, 5 | io::{self, Write}, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use clap::{Parser, Subcommand}; 10 | use colored::*; 11 | use red4_update::{get_info, Diff, FileInfo}; 12 | 13 | #[derive(Parser)] 14 | #[command(author, version, about, long_about = None)] 15 | struct Cli { 16 | #[command(subcommand)] 17 | command: Option, 18 | } 19 | 20 | #[derive(Subcommand)] 21 | enum Commands { 22 | /// generate patch diff 23 | Generate { 24 | /// Path to old Game folder 25 | #[arg(short, long)] 26 | old_dir: PathBuf, 27 | 28 | /// Path to new Game folder 29 | #[arg(short, long)] 30 | new_dir: PathBuf, 31 | }, 32 | /// checks a mod archive 33 | Check { 34 | /// Path to a folder with archives to check 35 | path: Option, 36 | }, 37 | } 38 | 39 | fn main() { 40 | simple_logger::init().unwrap(); 41 | 42 | let cli = Cli::parse(); 43 | 44 | // You can check for the existence of subcommands, and if found use their 45 | // matches just as you would the top level cmd 46 | match &cli.command { 47 | Some(Commands::Generate { old_dir, new_dir }) => { 48 | let diff = generate_diff(old_dir, new_dir); 49 | 50 | // write 51 | log::info!("Creating json ..."); 52 | if let Ok(json) = serde_json::to_string_pretty(&diff) { 53 | // to file 54 | let mut file = File::create("diff.json").expect("Failed to create file"); 55 | file.write_all(json.as_bytes()) 56 | .expect("Failed to write file"); 57 | } else { 58 | log::error!("Failed to serialize diff.") 59 | } 60 | } 61 | Some(Commands::Check { path: path_option }) => { 62 | let mut path = PathBuf::from(""); 63 | if let Some(p) = path_option { 64 | path = p.to_path_buf(); 65 | } 66 | if !path.exists() { 67 | if let Ok(cwd) = env::current_dir() { 68 | path = cwd; 69 | } else { 70 | log::error!("No input path found"); 71 | return; 72 | } 73 | } 74 | 75 | check_path_for_updates(path); 76 | 77 | println!("Press any button to continue ..."); 78 | let mut input = String::new(); 79 | io::stdin() 80 | .read_line(&mut input) 81 | .expect("error: unable to read user input"); 82 | } 83 | None => {} 84 | } 85 | } 86 | 87 | fn check_path_for_updates(path: PathBuf) { 88 | log::info!("Loading Hashes ..."); 89 | let hash_map = red4lib::get_red4_hashes(); 90 | 91 | log::info!("Loading Diff ..."); 92 | let bytes = include_bytes!("diff.json"); 93 | let diff: Diff = serde_json::from_slice(bytes).expect("Could not deserialize diff"); 94 | 95 | log::info!("Parsing mods ..."); 96 | let mut check_map: HashMap = HashMap::default(); 97 | get_info(&path, &mut check_map, &hash_map); 98 | 99 | log::info!("Creating report ..."); 100 | let mut report: HashMap = HashMap::default(); 101 | for (hash, file_info) in check_map { 102 | let archive = file_info.archive_name.clone(); 103 | if !report.contains_key(&archive) { 104 | let d = Diff::default(); 105 | report.insert(archive.clone(), d); 106 | } 107 | 108 | if let Some(d) = report.get_mut(&archive) { 109 | if diff.deleted.contains_key(&hash) { 110 | d.deleted.insert(hash, file_info.clone()); 111 | } 112 | if diff.added.contains_key(&hash) { 113 | d.added.insert(hash, file_info.clone()); 114 | } 115 | if diff.changed.contains_key(&hash) { 116 | d.changed.insert(hash, file_info.clone()); 117 | } 118 | } 119 | } 120 | 121 | log::info!("Creating json ..."); 122 | if let Ok(json) = serde_json::to_string_pretty(&report) { 123 | // to file 124 | let mut file = File::create("report.json").expect("Failed to create file"); 125 | file.write_all(json.as_bytes()) 126 | .expect("Failed to write file"); 127 | } else { 128 | log::error!("Failed to serialize report.") 129 | } 130 | 131 | // report to console 132 | println!(); 133 | println!("The following mods may need to be updated:"); 134 | let mut keys = report.keys().collect::>(); 135 | keys.sort(); 136 | for key in keys { 137 | if let Some(info) = report.get(key) { 138 | if info.deleted.len() + info.added.len() + info.changed.len() == 0 { 139 | continue; 140 | } 141 | 142 | println!(); 143 | println!("{}", key.blue().bold()); 144 | if !info.deleted.is_empty() { 145 | println!("\t{}", "deleted:".red()); 146 | for i in info.deleted.values() { 147 | println!("\t\t{}", i.name); 148 | } 149 | } 150 | 151 | if !info.added.is_empty() { 152 | println!("\t{}", "added:".green()); 153 | for i in info.added.values() { 154 | println!("\t\t{}", i.name); 155 | } 156 | } 157 | 158 | if !info.changed.is_empty() { 159 | println!("\t{}", "changed:".yellow()); 160 | for i in info.changed.values() { 161 | println!("\t\t{}", i.name); 162 | } 163 | } 164 | } 165 | } 166 | } 167 | 168 | fn generate_diff(old_dir: &Path, new_dir: &Path) -> Diff { 169 | log::info!("Loading Hashes ..."); 170 | let hash_map = red4lib::get_red4_hashes(); 171 | 172 | let old_map_path = PathBuf::from("old_file_map.json"); 173 | let new_map_path = PathBuf::from("new_file_map.json"); 174 | 175 | // assume this is a game folder 176 | let mut old_file_map: HashMap = HashMap::default(); 177 | if old_map_path.exists() { 178 | let json = fs::read_to_string(old_map_path).expect("ould not read old map"); 179 | old_file_map = serde_json::from_str(json.as_str()).expect("Could not deserialize old map"); 180 | log::info!("Read old map from file"); 181 | } else { 182 | get_info( 183 | &old_dir.join("archive").join("pc").join("content"), 184 | &mut old_file_map, 185 | &hash_map, 186 | ); 187 | get_info( 188 | &old_dir.join("archive").join("pc").join("ep1"), 189 | &mut old_file_map, 190 | &hash_map, 191 | ); 192 | 193 | log::info!("Creating old_file_map ..."); 194 | if let Ok(json) = serde_json::to_string_pretty(&old_file_map) { 195 | // to file 196 | let mut file = File::create(old_map_path).expect("Failed to create file"); 197 | file.write_all(json.as_bytes()) 198 | .expect("Failed to write file"); 199 | } else { 200 | log::error!("Failed to serialize diff.") 201 | } 202 | } 203 | 204 | // assume this is a game folder 205 | let mut new_file_map: HashMap = HashMap::default(); 206 | if new_map_path.exists() { 207 | let json = fs::read_to_string(new_map_path).expect("ould not read new map"); 208 | new_file_map = serde_json::from_str(json.as_str()).expect("Could not deserialize new map"); 209 | log::info!("Read new map from file"); 210 | } else { 211 | get_info( 212 | &new_dir.join("archive").join("pc").join("content"), 213 | &mut new_file_map, 214 | &hash_map, 215 | ); 216 | get_info( 217 | &new_dir.join("archive").join("pc").join("ep1"), 218 | &mut new_file_map, 219 | &hash_map, 220 | ); 221 | 222 | log::info!("Creating new_file_map ..."); 223 | if let Ok(json) = serde_json::to_string_pretty(&new_file_map) { 224 | // to file 225 | let mut file = File::create(new_map_path).expect("Failed to create file"); 226 | file.write_all(json.as_bytes()) 227 | .expect("Failed to write file"); 228 | } else { 229 | log::error!("Failed to serialize diff.") 230 | } 231 | } 232 | 233 | // diff the maps 234 | log::info!("Checking deleted files ..."); 235 | let deleted_vec: Vec = old_file_map 236 | .iter() 237 | .filter(|(k, _v)| !new_file_map.contains_key(k)) 238 | .map(|f| f.1.clone()) 239 | .collect(); 240 | log::info!("Found {} deleted files", deleted_vec.len()); 241 | 242 | log::info!("Checking added files ..."); 243 | let added_vec: Vec = new_file_map 244 | .iter() 245 | .filter(|(k, _v)| !old_file_map.contains_key(k)) 246 | .map(|f| f.1.clone()) 247 | .collect(); 248 | log::info!("Found {} added files", added_vec.len()); 249 | 250 | log::info!("Checking changed files ..."); 251 | let changed_vec: Vec = old_file_map 252 | .iter() 253 | .filter(|(k, old)| { 254 | if let Some(new) = new_file_map.get(k) { 255 | if old.sha1 != new.sha1 { 256 | return true; 257 | } 258 | } 259 | false 260 | }) 261 | .map(|f| f.1.clone()) 262 | .collect(); 263 | log::info!("Found {} changed files", changed_vec.len()); 264 | 265 | let mut deleted: HashMap = HashMap::default(); 266 | for i in deleted_vec { 267 | deleted.insert(i.hash, i); 268 | } 269 | let mut added: HashMap = HashMap::default(); 270 | for i in added_vec { 271 | added.insert(i.hash, i); 272 | } 273 | let mut changed: HashMap = HashMap::default(); 274 | for i in changed_vec { 275 | changed.insert(i.hash, i); 276 | } 277 | 278 | Diff { 279 | deleted, 280 | added, 281 | changed, 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /tweak-doxygen.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "tweak-doxygen" 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /tweak-doxygen/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .vscode/launch.json 3 | -------------------------------------------------------------------------------- /tweak-doxygen/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "same-file" 7 | version = "1.0.6" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 10 | dependencies = [ 11 | "winapi-util", 12 | ] 13 | 14 | [[package]] 15 | name = "tweak-doxygen" 16 | version = "0.1.0" 17 | dependencies = [ 18 | "walkdir", 19 | ] 20 | 21 | [[package]] 22 | name = "walkdir" 23 | version = "2.5.0" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 26 | dependencies = [ 27 | "same-file", 28 | "winapi-util", 29 | ] 30 | 31 | [[package]] 32 | name = "winapi-util" 33 | version = "0.1.9" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 36 | dependencies = [ 37 | "windows-sys", 38 | ] 39 | 40 | [[package]] 41 | name = "windows-sys" 42 | version = "0.59.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 45 | dependencies = [ 46 | "windows-targets", 47 | ] 48 | 49 | [[package]] 50 | name = "windows-targets" 51 | version = "0.52.6" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 54 | dependencies = [ 55 | "windows_aarch64_gnullvm", 56 | "windows_aarch64_msvc", 57 | "windows_i686_gnu", 58 | "windows_i686_gnullvm", 59 | "windows_i686_msvc", 60 | "windows_x86_64_gnu", 61 | "windows_x86_64_gnullvm", 62 | "windows_x86_64_msvc", 63 | ] 64 | 65 | [[package]] 66 | name = "windows_aarch64_gnullvm" 67 | version = "0.52.6" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 70 | 71 | [[package]] 72 | name = "windows_aarch64_msvc" 73 | version = "0.52.6" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 76 | 77 | [[package]] 78 | name = "windows_i686_gnu" 79 | version = "0.52.6" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 82 | 83 | [[package]] 84 | name = "windows_i686_gnullvm" 85 | version = "0.52.6" 86 | source = "registry+https://github.com/rust-lang/crates.io-index" 87 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 88 | 89 | [[package]] 90 | name = "windows_i686_msvc" 91 | version = "0.52.6" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 94 | 95 | [[package]] 96 | name = "windows_x86_64_gnu" 97 | version = "0.52.6" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 100 | 101 | [[package]] 102 | name = "windows_x86_64_gnullvm" 103 | version = "0.52.6" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 106 | 107 | [[package]] 108 | name = "windows_x86_64_msvc" 109 | version = "0.52.6" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 112 | -------------------------------------------------------------------------------- /tweak-doxygen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tweak-doxygen" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | walkdir = "2" 10 | -------------------------------------------------------------------------------- /tweak-doxygen/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::fs; 4 | use std::fs::File; 5 | use std::io::{BufRead, BufReader}; 6 | use std::io::{BufWriter, Write}; 7 | use std::path::Path; 8 | use std::path::PathBuf; 9 | use walkdir::WalkDir; 10 | 11 | fn main() { 12 | let args: Vec = env::args().collect(); 13 | 14 | let mut in_path = env::current_dir().expect("No working directory"); 15 | 16 | if args.len() > 1 { 17 | in_path = (&args[1]).into(); 18 | } 19 | 20 | let mut out_path = PathBuf::from("./_tweak_cs"); 21 | if args.len() > 2 { 22 | out_path = (&args[2]).into(); 23 | } 24 | 25 | let mut map = get_lines(in_path.as_os_str().to_str().expect("no usable input path")); 26 | convert_to_cs(&mut map, out_path); 27 | 28 | println!("Done!"); 29 | } 30 | 31 | #[derive(Default)] 32 | struct PackageInfo { 33 | classes: Vec, 34 | usings: Vec, 35 | } 36 | 37 | fn get_lines(path: &str) -> HashMap { 38 | let mut map: HashMap = HashMap::new(); 39 | 40 | println!("Starting collecting ..."); 41 | 42 | for file in WalkDir::new(path) 43 | .into_iter() 44 | .filter_map(Result::ok) 45 | .filter(|e| !e.file_type().is_dir()) 46 | .filter(|e| e.path().extension().is_some()) 47 | .filter(|e| { 48 | e.path() 49 | .extension() 50 | .unwrap() 51 | .to_str() 52 | .unwrap() 53 | .contains("tweak") 54 | }) 55 | { 56 | //println!("Processing {} ...", file.path().display()); 57 | 58 | let mut namespace = String::from(""); 59 | let mut usings: Vec = vec![]; 60 | let mut inner_lines = vec![]; 61 | 62 | // read lines in file 63 | let mut skipping = false; 64 | let reader = BufReader::new(fs::File::open(file.path()).unwrap()); 65 | for (linen, rawline) in reader.lines().enumerate() { 66 | let f = file.path().display().to_string(); 67 | let mut line = rawline.unwrap_or_else(|_| panic!("ERROR at {f}:{linen}")); 68 | 69 | // end block comments 70 | if skipping { 71 | if line.starts_with("*/") { 72 | skipping = false; 73 | // still continue with rest of line 74 | line = line[2..].to_string(); 75 | } else if line.ends_with("*/") { 76 | skipping = false; 77 | // can be skipped 78 | continue; 79 | } else if line.contains("/*") && line.contains("*/") { 80 | // do nothing 81 | println!("???") 82 | } else if line.contains("*/") { 83 | skipping = false; 84 | // still continue with rest of line 85 | let idx = line.find("*/").unwrap() + 2; 86 | line = line[idx..].to_string(); 87 | } 88 | 89 | // nothing found to end skipping 90 | if skipping { 91 | continue; 92 | } 93 | } 94 | 95 | // start block comments 96 | if !skipping { 97 | if line.starts_with("/*") && !line.contains("*/") { 98 | skipping = true; 99 | // can be skipped and continue skipping 100 | continue; 101 | } else if line.starts_with("/*") && line.contains("*/") { 102 | // block comment in one line 103 | // do nothing but ignore the comment 104 | // still continue with rest of line 105 | let idx = line.find("*/").unwrap() + 2; 106 | line = line[idx..].to_string(); 107 | } else if line.contains("/*") && !line.contains("*/") { 108 | skipping = true; 109 | 110 | // println!( 111 | // "WARNING: blockcomment across multiple lines at {} in {}", 112 | // linen, 113 | // file.path().display() 114 | // ); 115 | 116 | // still evaluate start of the line 117 | let idx = line.find("/*").unwrap(); 118 | line = line[..idx].to_string(); 119 | } 120 | } 121 | 122 | // namespaces 123 | if line.starts_with("package ") { 124 | namespace = line.clone()["package ".len()..].trim_end().to_string(); 125 | } 126 | 127 | // usings 128 | if let Some(stripped) = line.strip_prefix("using ") { 129 | usings = stripped 130 | //.trim_end() 131 | .split(',') 132 | .map(|s| s.trim().to_owned()) 133 | .collect::>(); 134 | } 135 | 136 | // skip specific lines 137 | if line.is_empty() 138 | || line.starts_with('\t') 139 | || line.starts_with(' ') 140 | || line.starts_with('{') 141 | || line.starts_with('}') 142 | || line.starts_with('[') 143 | || line.starts_with(']') 144 | || line.starts_with("package ") 145 | || line.starts_with("using ") 146 | || line.starts_with("//") 147 | || line.contains('=') 148 | { 149 | continue; 150 | } 151 | 152 | // classes 153 | 154 | // Sanitize 155 | let mut rawclass = line.clone(); 156 | 157 | // clean comments e.g. OverrideAuthorizationClassHack : DeviceQuickHack // ---> obsolete 158 | if rawclass.contains("//") { 159 | let s: String = rawclass.split("//").take(1).collect(); 160 | //println!("INFO: sanitize [{}] in {}", c, file.path().display()); 161 | rawclass = s.trim_end().to_string(); 162 | } 163 | 164 | // clean oneliners e.g. TauntLMGOpen03 : TauntLMGOpen01 {} 165 | if rawclass.contains("{}") { 166 | let s: String = rawclass.split("{}").take(1).collect(); 167 | //println!("INFO: sanitize [{}] in {}", c, file.path().display()); 168 | rawclass = s.trim_end().to_string(); 169 | } 170 | 171 | inner_lines.push(rawclass); 172 | } 173 | 174 | // error if no namespace 175 | if namespace.is_empty() { 176 | println!("ERROR: no namespace in file {}", file.path().display()); 177 | continue; 178 | } 179 | 180 | // add to map 181 | // add namespace 182 | if !map.contains_key(&namespace) { 183 | map.insert(namespace.to_string(), PackageInfo::default()); 184 | } 185 | // add class to namespace 186 | for c in inner_lines { 187 | if !map[&namespace].classes.contains(&c) { 188 | map.get_mut(&namespace).unwrap().classes.push(c); 189 | } 190 | } 191 | // add usings to namespace 192 | for u in usings { 193 | if !map[&namespace].usings.contains(&u.to_string()) { 194 | map.get_mut(&namespace).unwrap().usings.push(u.to_string()); 195 | } 196 | } 197 | } 198 | 199 | map 200 | } 201 | 202 | fn convert_to_cs(map: &mut HashMap, out_path: PathBuf) { 203 | // check lowercase duplicates 204 | let mut check = vec![]; 205 | 206 | println!("Starting printing ..."); 207 | let outpath = Path::new(&out_path); 208 | fs::create_dir_all(outpath).expect("Error creating folder"); 209 | 210 | for (key, package) in map { 211 | if check.contains(&key.to_lowercase()) { 212 | panic!("DUPLICATE {key} ..."); 213 | } 214 | 215 | //println!("Processing {key} ..."); 216 | 217 | let file = File::create(outpath.join(format!("{key}.cs"))) 218 | .unwrap_or_else(|_| panic!("ERROR: Failed to create file {key}")); 219 | let mut writer = BufWriter::new(file); 220 | 221 | // write to file 222 | // usings //using System.Collections.Generic; 223 | for u in package.usings.iter() { 224 | writer 225 | .write_all(format!("using {u};\n").as_bytes()) 226 | .unwrap(); 227 | } 228 | 229 | // namespace 230 | writer 231 | .write_all(format!("namespace {key};\n\n").as_bytes()) 232 | .unwrap(); 233 | 234 | // classes 235 | for c in package.classes.iter() { 236 | writer 237 | .write_all(format!("public class {c} {{ }}\n").as_bytes()) 238 | .unwrap(); 239 | } 240 | 241 | check.push(key.to_lowercase()); 242 | } 243 | } 244 | --------------------------------------------------------------------------------