diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..bd7375a --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,22 @@ +[bumpversion] +current_version = 1.6.0 +parse = (?P\d+)\.(?P\d+)\.(?P\d+)(-(?P[a-z]+))? +commit = True +tag = False +serialize = + {major}.{minor}.{patch}-{release} + {major}.{minor}.{patch} + +[bumpversion:part:release] +optional_value = gamma +values = + beta + gamma + +[bumpversion:file:disenchanter.rb] +search = current_version = 'v{current_version}' +replace = current_version = 'v{new_version}' + +[bumpversion:file:sonar-project.properties] +search = sonar.projectVersion=v{current_version} +replace = sonar.projectVersion=v{new_version} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bcdd309 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches-ignore: + - 'dependabot**' + - 'release**' + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +jobs: + analyze: + name: Analyze + if: github.event_name != 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: read-all + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.2.3 + bundler-cache: true + + - name: Install dependencies + run: bundle install + + - name: RuboCop scan + run: bundle exec rubocop --format json -o rubocop-report.json || echo "Failed on RuboCop rules" + + - name: Upload RuboCop report + uses: actions/upload-artifact@v2 + with: + name: rubocop-report + path: rubocop-report.json + + - name: SonarQube scan + uses: sonarsource/sonarqube-scan-action@master + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }} + + - name: Quality gate check + if: github.event_name != 'pull_request' || github.event_name == 'workflow_dispatch' + uses: sonarsource/sonarqube-quality-gate-action@master + timeout-minutes: 5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.gitignore b/.gitignore index 32268dd..35eebe5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ disenchanter_up.exe tmpin tmpout +disenchanter_*.json +settings.json +rubocop-report.json + *.gem *.rbc /.config diff --git a/.rubocop.yml b/.rubocop.yml index 4d0d314..d18ff8a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,2 +1,20 @@ -Style/EndOfLine: +AllCops: + NewCops: enable +Layout/EndOfLine: EnforcedStyle: lf +Metrics/MethodLength: + CountAsOne: ['array', 'method_call', 'hash'] + Max: 35 + Severity: refactor +Metrics/CyclomaticComplexity: + Max: 15 + Severity: refactor +Metrics/PerceivedComplexity: + Max: 15 + Severity: refactor +Metrics/AbcSize: + CountRepeatedAttributes: false + Max: 30 + Severity: refactor +Style/EmptyElse: + AllowComments: true \ No newline at end of file diff --git a/.rufo b/.rufo index c0f958c..36dfe81 100644 --- a/.rufo +++ b/.rufo @@ -1 +1,2 @@ -quote_style :single \ No newline at end of file +quote_style :single +trailing_commas false diff --git a/.wakatime-project b/.wakatime-project new file mode 100644 index 0000000..67e25bb --- /dev/null +++ b/.wakatime-project @@ -0,0 +1 @@ +disenchanter diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..589845d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,109 @@ +# Changelog + +All notable changes to this project will be documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). Related: [versioning strategy](./VERSIONING.md). + +## v1.6.0 - Feb 17, 2024 +### Added +- #35 Disenchanter can be started from anywhere now + - It will try to find your League Client via registry > start menu > default path > locally +- #57 Rerolling owned esports emotes that cannot be disenchanted +- #19 Tacticians can now be disenchanted +- Shortcut to your [Mastery Chart](https://masterychart.com) profile +- #27 Proper changelog and versioning info +### Changed +- (dev) Code is split in modules now instead of stuffed into a single script +- (dev) Switched from ocra to ocran for building the executable +### Fixed +- #142 Script won't crash if you don't have a summoner name + - Essentially Riot ID support +- #87 Shards of champions with no mastery will no longer falsely be considered for disenchanting +- #127 Champion permanents can be disenchanted again +- Reliability improvements via more flexible recipe and currency detection + - #128 Allows properly disenchanting things like blue essence granting summoner icons + +## v1.5.0 - Jul 26, 2022 +### Added +- Support for disenchanting champion, skin, ward skin and eternal permanents +- Mastery token upgrades will also use champion permanents +- Debug options for troubleshooting +- (dev) Linting and formatting rules +- Demo image in `README` :) +### Changed +- Smarter detection for items that aren't owned yet +- Blue essence directly looted from capsules is now included in stats +- Loot will be refreshed more frequently to prevent calls on stale data +### Fixed +- Clarity when upgrading mastery tokens +- Some chest display names like Honor Capsules will have the name manually injected instead +- Faulty crafting recipes adjusted +- Typos removed + +## v1.4.0 - Jul 14, 2022 +### Added +- Disenchanting summoner icons +- Efficient mastery 6/7 token upgrades + - Does not support champion permanents (yet) +### Changed +- Menu order is now like in the Client's loot tab + - Materials are in separate submenu now +### Fixed +- #21 Bugfix for event token crafting +- Smarter version check for people directly running the Ruby script +- Code cleanup, minor bugfixes + +## v1.3.2 - Jun 30, 2022 +### Fixed +- #10 Collection feature properly retains one shard per champion + +## v1.3.1 - Jun 29, 2022 +### Fixed +- Fixed "keep shards of champions you don't own yet" option +- Fixed in-place updater + added backwards compatibility + +## v1.3.0 - Jun 28, 2022 +### Added +- In-place updating + - Future versions can replace the old script with the latest version +- More things to disenchant: + - Ward skins + - Skins + - Eternals +### Changed +- Swapped to new, less bloated menu style +### Fixed +- Reliability improvements to existing options + +## v1.2.3 - Jun 21, 2022 +### Fixed +- Clarity in wording and visuals +- Fixes to conservative options + +## v1.2.2 - Jun 21, 2022 +### Fixed +- Failing one step will no longer break the script but fall back to the main menu + +## v1.2.1 - Jun 19, 2022 +### Fixed +- Bugfix for capsule handling +### Docs +- Added info on malware false positives + +## v1.2.0 - Jun 19, 2022 +### Added +- Mythic Essence crafting to random skin shards, blue essence and orange essence +- Opening of keyless capsules +- Colors in terminal +- User can now specify how many event tokens should be used + +## v1.1.1 - Jun 18, 2022 +### Fixed +- Bugfix in stat submission + +## v1.1.0 - Jun 17, 2022 +### Added +- Will notify the user whether the latest version is running + +## v1.0.0 - Jun 17, 2022 +### Added +- Initial Release \ No newline at end of file diff --git a/Gemfile b/Gemfile index ef6f980..8ecfa9d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,18 +1,22 @@ +# frozen_string_literal: true + source 'https://rubygems.org' -git_source(:github) { |repo| 'https://github.com/#{repo}.git' } -ruby '3.1.2' +git_source(:github) { |_repo| 'https://github.com/marvinscham/disenchanter.git' } +ruby '3.2.3' -gem 'base64', '~> 0.1.1' -gem 'colorize', '~> 0.8.1' +gem 'base64', '~> 0.2' +gem 'colorize', '~> 1.1' gem 'json', '~> 2.6' gem 'launchy', '~> 2.5' +gem 'openssl', '= 3.1.0' gem 'open-uri', '~> 0.2.0' +gem 'win32-shortcut', '~> 0.3.0' group :development do # Builds windows executable - gem 'ocra', '1.3.11', require: false + gem 'ocran', '1.3.15', require: false # Ruby formatter, config in .rufo gem 'rufo', '>= 0.13.0', require: false # Ruby linter, config in .rubocop - gem 'rubocop', '~> 1.50', require: false + gem 'rubocop', '~> 1.60', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 8934797..738ae96 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,64 +1,72 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.1) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) - base64 (0.1.1) - colorize (0.8.1) - date (3.2.2) - json (2.6.3) + base64 (0.2.0) + colorize (1.1.0) + date (3.3.4) + json (2.7.1) + language_server-protocol (3.17.0.3) launchy (2.5.2) addressable (~> 2.8) - ocra (1.3.11) + ocran (1.3.15) open-uri (0.2.0) stringio time uri - parallel (1.22.1) - parser (3.2.2.0) + openssl (3.1.0) + parallel (1.24.0) + parser (3.3.0.5) ast (~> 2.4.1) - public_suffix (5.0.1) + racc + public_suffix (5.0.4) + racc (1.7.3) rainbow (3.1.1) - regexp_parser (2.8.0) - rexml (3.2.5) - rubocop (1.50.2) + regexp_parser (2.9.0) + rexml (3.2.6) + rubocop (1.60.2) json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.2.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.0, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.28.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) ruby-progressbar (1.13.0) - rufo (0.16.0) - stringio (3.0.2) - time (0.2.0) + rufo (0.17.1) + stringio (3.1.0) + time (0.3.0) date - unicode-display_width (2.4.2) - uri (0.11.0) + unicode-display_width (2.5.0) + uri (0.13.0) + win32-shortcut (0.3.0) PLATFORMS - x64-unknown + x64-mingw-ucrt x64-unknown x86_64-linux DEPENDENCIES - base64 (~> 0.1.1) - colorize (~> 0.8.1) + base64 (~> 0.2) + colorize (~> 1.1) json (~> 2.6) launchy (~> 2.5) - ocra (= 1.3.11) + ocran (= 1.3.15) open-uri (~> 0.2.0) - rubocop (~> 1.50) + openssl (= 3.1.0) + rubocop (~> 1.60) rufo (>= 0.13.0) + win32-shortcut (~> 0.3.0) RUBY VERSION - ruby 3.1.2p20 + ruby 3.2.3p157 BUNDLED WITH - 2.3.15 + 2.4.19 diff --git a/README.md b/README.md index 077687c..c6cc1f9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@

- +

Disenchanter

-![Patch](https://img.shields.io/badge/league%20patch-14.1-brightgreen) +![Patch](https://img.shields.io/badge/league%20patch-14.2-brightgreen) ![Release](https://img.shields.io/github/v/release/marvinscham/disenchanter) ![Last Commit](https://img.shields.io/github/last-commit/marvinscham/disenchanter) @@ -19,7 +19,7 @@ Mass disenchant LoL loot like champion shards, eternals, mythic essence and more! -[](https://www.buymeacoffee.com/mscham) +[](https://www.buymeacoffee.com/mscham) Based on [Anujan/disenchant-champ-shards](https://github.com/Anujan/disenchant-champ-shards). @@ -31,13 +31,15 @@ Based on [Anujan/disenchant-champ-shards](https://github.com/Anujan/disenchant-c Download the pre-built `disenchanter.exe` from the [Latest Release](https://github.com/marvinscham/disenchanter/releases). -Put the `disenchanter.exe` file in the same folder as your `LeagueClient.exe`, e.g. `C:\Riot Games\League of Legends` and run it. Make sure your League Client is running without admin privileges and you're logged in before running Disenchanter. +Start your League Client **without admin privileges** and log into your account, then start the script. + +## Details The script is interactive and will guide you through the process with simple `[y|n]` questions and mode options. Before you disenchant or craft anything, you will be asked to confirm the action in a magenta colored message with a big `CONFIRM:` banner so don't be scared to explore the different options! Once you're finished, you can _optionally_ contribute your (anonymous) stats to the [Global Stats](https://github.com/marvinscham/disenchanter/wiki/Stats). ([Details](https://github.com/marvinscham/disenchanter/wiki/Stat-Collection)) -![Demo](https://raw.githubusercontent.com/marvinscham/disenchanter/main/disenchanter.png) +![Demo](./assets/disenchanter.png) ## Is this a virus? @@ -51,43 +53,26 @@ The script triggers the same server requests as you would in your League Client. ## Features -- _Note: no longer supports event tokens since Riot updated event passes_ - Materials - - Craft Mythic Essence to Skins or Blue/Orange Essence - - Combine Key Fragments - - Open keyless capsules - - - Upgrade Mastery Tokens - + - Upgrade Mastery Tokens efficiently - Champion Shards - - Disenchant all - - Keep one for champions you don't own yet - - Keep enough (1/2) for champions you own mastery 6/7 tokens for - - Keep enough (1/2) to fully master champions at least at mastery level x (select from 1 to 6) - - Keep enough (1/2) to fully master all champions (only disenchant shards that have no possible use) - - Keep one of each champion regardless of mastery - - Manual exceptions - - Disenchant various items - - Eternals - - Emotes - - Ward Skins - - Summoner Icons + - Tacticians ## Problems, Bugs and Feature Suggestions @@ -95,13 +80,11 @@ Something isn't working properly or you'd like to see a feature that isn't yet s - [Create an issue](https://github.com/marvinscham/disenchanter/issues/new/choose) - (**If you have no GitHub account**) hit me up at dev[at]marvinscham.de - - Open a pull request with your contribution. ## ❤ Sponsors ❤ - Ze Interrupter - - tsunamihorseracing ## Disclaimer diff --git a/VERSIONING.md b/VERSIONING.md new file mode 100644 index 0000000..79d2120 --- /dev/null +++ b/VERSIONING.md @@ -0,0 +1,14 @@ +# Versioning + +Disenchanter's versioning is based on [Semantic Versioning](https://semver.org/). + +The version number is structured as `MAJOR.MINOR.PATCH`: +- `MAJOR` version is incremented on changes breaking the current usage flow +- `MINOR` version is incremented on backwards compatible functionality additions +- `PATCH` version is incremented on backwards compatible bug fixes + +Notable changes in documentation will be documented in the [changelog](./CHANGELOG.md) for historical insights. + +## Versioning Workflow + +Versioning is handled via [bumpversion](https://github.com/peritus/bumpversion), configured in `.bumpversion.cfg`. \ No newline at end of file diff --git a/BE_icon.ico b/assets/BE_icon.ico similarity index 100% rename from BE_icon.ico rename to assets/BE_icon.ico diff --git a/disenchanter.png b/assets/disenchanter.png similarity index 100% rename from disenchanter.png rename to assets/disenchanter.png diff --git a/assets/kofi-button.png b/assets/kofi-button.png new file mode 100644 index 0000000..9982fb5 Binary files /dev/null and b/assets/kofi-button.png differ diff --git a/build.cmd b/build.cmd deleted file mode 100644 index 4908269..0000000 --- a/build.cmd +++ /dev/null @@ -1,3 +0,0 @@ -start cmd.exe @cmd /k "ocra disenchanter_up.rb --gemfile .\Gemfile --dll ruby_builtin_dlls\libssp-0.dll --dll ruby_builtin_dlls\libgmp-10.dll --dll ruby_builtin_dlls\libgcc_s_seh-1.dll --dll ruby_builtin_dlls\libwinpthread-1.dll --dll ruby_builtin_dlls\libssl-1_1-x64.dll --dll ruby_builtin_dlls\libcrypto-1_1-x64.dll --icon BE_icon.ico && exit" -timeout /T 20 /nobreak -start cmd.exe @cmd /k "ocra disenchanter.rb --gemfile .\Gemfile --dll ruby_builtin_dlls\libssp-0.dll --dll ruby_builtin_dlls\libgmp-10.dll --dll ruby_builtin_dlls\libgcc_s_seh-1.dll --dll ruby_builtin_dlls\libwinpthread-1.dll --dll ruby_builtin_dlls\libssl-1_1-x64.dll --dll ruby_builtin_dlls\libcrypto-1_1-x64.dll --icon BE_icon.ico && exit" diff --git a/disenchanter.rb b/disenchanter.rb deleted file mode 100644 index 1ed303d..0000000 --- a/disenchanter.rb +++ /dev/null @@ -1,1416 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "net/https" -require "base64" -require "json" -require "colorize" -require "launchy" -require "open-uri" - -def run - unless File.exist?("build.cmd") - set_globals - current_version = "v1.5.0" - - puts "Hi! :)".light_green - puts "Running Disenchanter #{current_version}".light_blue - puts "You can exit this script at any point by pressing ".light_blue + - "[CTRL + C]".light_white + ".".light_blue - check_update(current_version) - puts $sep - - summoner = get_current_summoner - if summoner["displayName"].nil? || summoner["displayName"].empty? - puts "Could not grab summoner info. Try restarting your League Client.".light_red - ask "Press Enter to exit.".cyan - exit 1 - end - puts "\nYou're logged in as #{summoner["displayName"]}.".light_blue - puts $sep - puts "\nFeel free to try the options, no actions will be taken until you confirm a banner like this:".light_blue - puts "CONFIRM: Perform this action? [y|n]".light_magenta - puts $sep - - done = false - things_todo = { - "1" => "Materials", - "2" => "Champions", - "3" => "Skins", - #"4" => "Tacticians", - "5" => "Eternals", - "6" => "Emotes", - "7" => "Ward Skins", - "8" => "Icons", - "s" => "Open Disenchanter Global Stats", - "r" => "Open GitHub repository", - "d" => "Debug Tools", - "x" => "Exit" - } - things_done = [] - - until done - todo_string = "" - things_todo.each do |k, v| - todo_string += "[#{k}] ".light_white - unless things_done.include? k - todo_string += "#{v}\n".light_cyan - else - todo_string += "#{v} (done)\n".light_green - end - end - - todo = - user_input_check( - "\nWhat would you like to do? (Hint: do Materials first so you don't miss anything!)\n\n".light_cyan + - todo_string + "Option: ", - things_todo.keys, - "", - "" - ) - things_done << todo - - puts $sep - puts - - puts "Option chosen: #{things_todo[todo]}".light_white - - case todo - when "1" - handle_materials - when "2" - handle_champions - when "3" - handle_skins - # when "4" - # handle_tacticians - when "5" - handle_eternals - when "6" - handle_emotes - when "7" - handle_ward_skins - when "8" - handle_icons - when "s" - open_stats - when "r" - open_github - when "d" - handle_debug - when "x" - done = true - end - refresh_loot - puts $sep - end - - puts "That's it!".light_green - if $actions > 0 - puts "We saved you about #{$actions * 3} seconds of waiting for animations to finish.".light_green - puts $sep - end - handle_stat_submission - puts "See you next time :)".light_green - ask "Press Enter to exit.".cyan - else - puts "Assuming build environment, skipping execution...".light_yellow - end -end - -def ask(q) - print(q) - q = gets - q.chomp -end - -def pad(str, len, right = true) - "%#{right ? "-" : ""}#{len}s" % str -end - -def set_globals - begin - $port, $token = read_lockfile - rescue StandardError - puts "Could not grab session!".light_red - puts "Make sure the script is in your League Client folder and that your Client is running.".light_red - ask "Press Enter to exit.".cyan - exit 1 - end - $host = "https://127.0.0.1:#{$port}" - $debug = false - - $sep = - "____________________________________________________________".light_black - - $actions = 0 - $s_disenchanted = 0 - $s_opened = 0 - $s_crafted = 0 - $s_redeemed = 0 - $s_blue_essence = 0 - $s_orange_essence = 0 - - $ans_yn = %w[y yes n no] - $ans_y = %w[y yes] - $ans_n = %w[n no] - $ans_yn_d = "[y|n]" -end - -def read_lockfile - contents = File.read("lockfile") - _leagueclient, _unk_port, port, password = contents.split(":") - token = Base64.encode64("riot:#{password.chomp}") - - [port, token] -end - -def create_client - Net::HTTP.start( - "127.0.0.1", - $port, - use_ssl: true, - verify_mode: OpenSSL::SSL::VERIFY_NONE - ) { |http| yield(http) } -end - -def req_set_headers(req) - req["Content-Type"] = "application/json" - req["Authorization"] = "Basic #{$token.chomp}" -end - -def check_update(version_local) - begin - uri = - URI( - "https://api.github.com/repos/marvinscham/disenchanter/releases/latest" - ) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - req = Net::HTTP::Get.new(uri, "Content-Type": "application/json") - res = http.request req - ans = JSON.parse(res.body) - - version_local = - Gem::Version.new(version_local.delete_prefix("v").delete_suffix("-beta")) - version_remote = - Gem::Version.new( - ans["tag_name"].delete_prefix("v").delete_suffix("-beta") - ) - - if version_remote > version_local - puts "New version #{ans["tag_name"]} available!".light_yellow - if ($ans_y).include? user_input_check( - "Would you like to download the new version now?", - $ans_yn, - $ans_yn_d - ) - `curl https://github.com/marvinscham/disenchanter/releases/download/#{ans["tag_name"]}/disenchanter_up.exe -L -o disenchanter_up.exe` - puts "Done downloading!".green - - pid = spawn("start cmd.exe @cmd /k \"disenchanter_up.exe\"") - Process.detach(pid) - puts "Exiting...".light_black - exit - end - elsif version_local > version_remote - puts "Welcome to the future!".light_magenta - puts "Latest remote version: v#{version_remote}".light_blue - else - puts "You're up to date!".green - end - rescue => exception - handle_exception(exception, "self update") - end -end - -def request_get(path) - create_client do |http| - uri = URI("#{$host}/#{path}") - req = Net::HTTP::Get.new(uri) - req_set_headers(req) - res = http.request req - JSON.parse(res.body) - end -end - -def request_post(path, body) - create_client do |http| - uri = URI("#{$host}/#{path}") - req = Net::HTTP::Post.new(uri, "Content-Type": "application/json") - req.body = body - req_set_headers(req) - res = http.request req - JSON.parse(res.body) - end -end - -def refresh_loot() - request_post("lol-loot/v1/refresh?force=true", "") -end - -def get_current_summoner() - request_get("lol-summoner/v1/current-summoner") -end - -def get_player_loot() - request_get("lol-loot/v1/player-loot") -end - -def get_champion_mastery(summoner_id) - request_get("lol-collections/v1/inventories/#{summoner_id}/champion-mastery") -end - -def get_loot_info(loot_id) - request_get("lol-loot/v1/player-loot/#{loot_id}") -end - -def get_recipes_for_item(loot_id) - request_get("lol-loot/v1/recipes/initial-item/#{loot_id}") -end - -def post_recipe(recipe, loot_ids, repeat) - $actions += repeat - - loot_id_string = "[\"" + Array(loot_ids).join("\", \"") + "\"]" - - op = - request_post( - "lol-loot/v1/recipes/#{recipe}/craft?repeat=#{repeat}", - loot_id_string - ) - - if $debug - File.open("disenchanter_post.json", "w") { |f| f.write(op.to_json) } - puts("Okay, written to disenchanter_post.json.") - end - op -end - -def user_input_check(question, answers, answerdisplay, color_preset = "default") - input = "" - - case color_preset - when "confirm" - question = - "CONFIRM: #{question} ".light_magenta + "#{answerdisplay}".light_white + - ": ".light_magenta - when "default" - question = - "#{question} ".light_cyan + "#{answerdisplay}".light_white + - ": ".light_cyan - end - - until (answers).include? input - input = ask question - unless (answers).include? input - puts "Invalid answer, options: ".light_red + - "#{answerdisplay}".light_white - end - end - - input -end - -def count_loot_items(loot_items) - count = 0 - unless loot_items.nil? || loot_items.empty? - loot_items.each { |loot| count += loot["count"] } - end - count -end - -def get_chest_name(loot_id) - chest_info = get_loot_info(loot_id) - return chest_info["localizedName"] if !chest_info["localizedName"].empty? - - catalogue = { - "CHEST_128" => "Champion Capsule", - "CHEST_129" => "Glorious Champion Capsule", - "CHEST_210" => "Honor Level 4 Orb", - "CHEST_211" => "Honor Level 5 Orb" - } - - return catalogue[loot_id] if catalogue.key?(loot_id) - - return loot_id -end - -def handle_exception(exception, name) - puts "An error occurred while handling #{name}.".light_red - puts "Please take a screenshot and create an issue at https://github.com/marvinscham/disenchanter/issues/new".light_red - puts "If you don't have a GitHub account, send it to dev@marvinscham.de".light_red - puts exception - puts "Skipping this step...".yellow -end - -def handle_materials - done = false - things_todo = { - "1" => "Mythic Essence", - "2" => "Event Tokens", - "3" => "Key Fragments", - "4" => "Capsules", - "5" => "Mastery Tokens", - "x" => "Back to main menu" - } - things_done = [] - - until done - todo_string = "" - things_todo.each do |k, v| - todo_string += "[#{k}] ".light_white - unless things_done.include? k - todo_string += "#{v}\n".light_cyan - else - todo_string += "#{v} (done)\n".light_green - end - end - - todo = - user_input_check( - "\nWhat would you like to do?\n\n".light_cyan + todo_string + - "Option: ", - things_todo.keys, - "", - "" - ) - things_done << todo - - puts $sep - puts - - puts "Option chosen: #{things_todo[todo]}".light_white - - case todo - when "1" - handle_mythic_essence - when "2" - handle_event_tokens - when "3" - handle_key_fragments - when "4" - handle_capsules - when "5" - handle_mastery_tokens - when "x" - done = true - end - puts $sep - end -end - -def handle_mythic_essence - begin - player_loot = get_player_loot - mythic_loot_id = "CURRENCY_mythic" - - loot_essence = player_loot.select { |l| l["lootId"] == mythic_loot_id } - loot_essence = loot_essence[0] - if !loot_essence.nil? && loot_essence["count"] > 0 - puts "Found #{loot_essence["count"]} Mythic Essence.".light_blue - craft_mythic_type_names = [ - "Blue Essence", - "Orange Essence", - "Random Skin Shards" - ] - - craft_mythic_type = - user_input_check( - "Okay, what would you like to craft?\n" + - "[1] #{craft_mythic_type_names[0]}\n" + - "[2] #{craft_mythic_type_names[1]}\n" + - "[3] #{craft_mythic_type_names[2]}\n" + "[x] Cancel\n", - %w[1 2 3 x], - "[1|2|3|x]" - ) - - unless craft_mythic_type == "x" - case craft_mythic_type - # Blue Essence, Orange Essence, Random Skin Shard - when "1" - recipe_target = "CURRENCY_champion" - when "2" - recipe_target = "CURRENCY_cosmetic" - when "3" - recipe_target = "CHEST_291" - end - - recipes = get_recipes_for_item(mythic_loot_id) - recipes = - recipes.select { |r| r["outputs"][0]["lootName"] == recipe_target } - unless recipes.length == 0 - recipe = recipes[0] - - puts "Recipe found: #{recipe["contextMenuText"]} for #{recipe["slots"][0]["quantity"]} Mythic Essence".light_blue - - craft_mythic_amount = - user_input_check( - "Alright, how much Mythic Essence should we use to craft #{craft_mythic_type_names[craft_mythic_type.to_i - 1]}?", - (1..loot_essence["count"].to_i) - .to_a - .append("all") - .map! { |n| n.to_s }, - "[1..#{loot_essence["count"]}|all]" - ) - - if craft_mythic_amount == "all" - craft_mythic_amount = loot_essence["count"] - end - craft_mythic_amount = craft_mythic_amount.to_i - - could_craft = - (craft_mythic_amount / recipe["slots"][0]["quantity"]).floor - unless could_craft < 1 - if ($ans_y).include? user_input_check( - "Craft #{could_craft * recipe["outputs"][0]["quantity"]} " + - "#{craft_mythic_type_names[craft_mythic_type.to_i - 1]} from " + - "#{(craft_mythic_amount / recipe["slots"][0]["quantity"]).floor * recipe["slots"][0]["quantity"]} Mythic Essence?", - $ans_yn, - $ans_yn_d, - "confirm" - ) - case craft_mythic_type - when "1" - $s_blue_essence += - could_craft * recipe["outputs"][0]["quantity"] - when "2" - $s_orange_essence += - could_craft * recipe["outputs"][0]["quantity"] - end - $s_crafted += could_craft - - post_recipe( - recipe["recipeName"], - mythic_loot_id, - (craft_mythic_amount / recipe["slots"][0]["quantity"]).floor - ) - puts "Done!".green - end - else - puts "Not enough Mythic Essence for that recipe.".yellow - end - else - puts "Recipes for #{craft_mythic_type_names[craft_mythic_type.to_i - 1]} seem to be unavailable.".yellow - end - else - puts "Mythic crafting canceled.".yellow - end - else - puts "Found no Mythic Essence to use.".yellow - end - rescue => exception - handle_exception(exception, "Mythic Essence") - end -end - -def handle_event_tokens - begin - player_loot = get_player_loot - - loot_event_token = - player_loot.select do |l| - l["type"] == "MATERIAL" && l["displayCategories"] == "CHEST" && - l["lootId"].start_with?("MATERIAL_") && - !l["lootId"].start_with?("MATERIAL_key") - end - loot_event_token = loot_event_token[0] - - if !loot_event_token.nil? && loot_event_token["count"] > 0 - puts "Found Event Tokens: #{loot_event_token["count"]}x #{loot_event_token["localizedName"]}".light_blue - token_recipes = get_recipes_for_item(loot_event_token["lootId"]) - - craft_tokens_type_names = [ - "Champion Shards and Blue Essence", - "Random Emotes" - ] - craft_tokens_type = - user_input_check( - "Okay, what would you like to craft?\n" + - "[1] #{craft_tokens_type_names[0]}\n" + - "[2] #{craft_tokens_type_names[1]}\n" + "[x] Cancel\n", - %w[1 2 x], - "[1|2|x]" - ) - - unless craft_tokens_type == "x" - # CHEST_187 = Random Emote - # CHEST_241 = Random Champion Shard - # CURRENCY_champion = Blue Essence - if craft_tokens_type == "1" - recipe_targets = %w[CHEST_241 CURRENCY_champion] - elsif craft_tokens_type == "2" - recipe_targets = %w[CHEST_187] - end - - token_recipes = token_recipes.select { |r| !r["outputs"][0].nil? } - - token_recipes = - token_recipes.select do |r| - recipe_targets.include? r["outputs"][0]["lootName"] - end - token_recipes = - token_recipes.sort_by { |r| r["slots"][0]["quantity"] }.reverse! - - token_recipes.each do |r| - puts "Recipe found: #{r["contextMenuText"]} for #{r["slots"][0]["quantity"]} Tokens".light_black - end - - craft_tokens_amount = - user_input_check( - "Alright, how many Event Tokens should we use to craft #{craft_tokens_type_names[craft_tokens_type.to_i - 1]}?", - (1..loot_event_token["count"].to_i) - .to_a - .append("all") - .map! { |n| n.to_s }, - "[1..#{loot_event_token["count"]}|all]" - ) - - if craft_tokens_amount == "all" - craft_tokens_amount = loot_event_token["count"] - end - craft_tokens_amount = craft_tokens_amount.to_i - - total_could_craft = 0 - - token_recipes.each do |r| - r["could_craft"] = ( - craft_tokens_amount / r["slots"][0]["quantity"] - ).floor - total_could_craft += r["could_craft"] - craft_tokens_amount -= - (craft_tokens_amount / r["slots"][0]["quantity"]).floor * - r["slots"][0]["quantity"] - if r["could_craft"] > 0 - puts "We could craft #{r["could_craft"]}x #{r["contextMenuText"]} for #{r["slots"][0]["quantity"]} Tokens each.".light_green - end - end - - token_recipes = token_recipes.select { |r| r["could_craft"] > 0 } - - if total_could_craft > 0 - if ($ans_y).include? user_input_check( - "Commit to forging?", - $ans_yn, - $ans_yn_d, - "confirm" - ) - token_recipes.each do |r| - if craft_tokens_type == "1" - $s_blue_essence += - r["outputs"][0]["quantity"] * r["could_craft"] - end - $s_crafted += r["could_craft"] - end - - threads = - token_recipes.map do |r| - Thread.new do - post_recipe( - r["recipeName"], - loot_event_token["lootId"], - r["could_craft"] - ) - end - end - threads.each(&:join) - puts "Done!".green - end - else - puts "Can't afford any recipe, skipping.".yellow - end - else - puts "Token crafting canceled.".yellow - end - else - puts "Found no Event Tokens.".yellow - end - rescue => exception - handle_exception(exception, "Event Tokens") - end -end - -def handle_key_fragments - begin - player_loot = get_player_loot - - loot_keys = - player_loot.select { |l| l["lootId"] == "MATERIAL_key_fragment" } - if count_loot_items(loot_keys) >= 3 - puts "Found #{count_loot_items(loot_keys)} key fragments.".light_blue - if ($ans_y).include? user_input_check( - "Craft #{(count_loot_items(loot_keys) / 3).floor} keys from #{count_loot_items(loot_keys)} key fragments?", - $ans_yn, - $ans_yn_d, - "confirm" - ) - $s_crafted += (count_loot_items(loot_keys) / 3).floor - post_recipe( - "MATERIAL_key_fragment_forge", - "MATERIAL_key_fragment", - (count_loot_items(loot_keys) / 3).floor - ) - puts "Done!".green - end - else - puts "Found less than 3 key fragments.".yellow - end - rescue => exception - handle_exception(exception, "Key Fragments") - end -end - -def handle_capsules - begin - player_loot = get_player_loot - - loot_capsules = - player_loot.select { |l| l["lootName"].start_with?("CHEST_") } - loot_capsules.each do |c| - recipes = get_recipes_for_item(c["lootId"]) - if recipes[0]["slots"].length > 1 || !recipes[0]["type"] == "OPEN" - c["needs_key"] = true - else - c["needs_key"] = false - end - end - loot_capsules = loot_capsules.select { |c| c["needs_key"] == false } - - if count_loot_items(loot_capsules) > 0 - puts "Found #{count_loot_items(loot_capsules)} capsules:".light_blue - loot_capsules.each do |c| - puts "#{c["count"]}x ".light_black + - "#{get_chest_name(c["lootId"])}".light_white - end - - if ($ans_y).include? user_input_check( - "Open #{count_loot_items(loot_capsules)} (keyless) capsules?", - $ans_yn, - $ans_yn_d, - "confirm" - ) - $s_opened += count_loot_items(loot_capsules) - threads = - loot_capsules.map do |c| - Thread.new do - res = post_recipe(c["lootId"] + "_OPEN", c["lootId"], c["count"]) - res["added"].each do |r| - if r["playerLoot"]["lootId"] == "CURRENCY_champion" - $s_blue_essence += r["deltaCount"] - end - end - end - end - threads.each(&:join) - puts "Done!".green - end - else - puts "Found no keyless capsules to open.".yellow - end - rescue => exception - handle_exception(exception, "Capsules") - end -end - -def handle_mastery_tokens - begin - player_loot = get_player_loot - loot_shards = player_loot.select { |l| l["type"] == "CHAMPION_RENTAL" } - loot_perms = player_loot.select { |l| l["type"] == "CHAMPION" } - - recipes6 = get_recipes_for_item("CHAMPION_TOKEN_6-1") - recipes7 = get_recipes_for_item("CHAMPION_TOKEN_7-1") - recipe6_cost = - recipes6.select do |r| - r["recipeName"] == "CHAMPION_TOKEN_6_redeem_withessence" - end - recipe6_cost = recipe6_cost[0]["slots"][1]["quantity"] - recipe7_cost = - recipes7.select do |r| - r["recipeName"] == "CHAMPION_TOKEN_7_redeem_withessence" - end - recipe7_cost = recipe7_cost[0]["slots"][1]["quantity"] - - loot_overall_tokens = - player_loot.select do |l| - (l["lootName"] == "CHAMPION_TOKEN_6") || - (l["lootName"] == "CHAMPION_TOKEN_7") - end - - puts "Found #{count_loot_items(loot_overall_tokens)} Mastery Tokens.".light_blue - - loot_mastery_tokens = - player_loot.select do |l| - (l["lootName"] == "CHAMPION_TOKEN_6" && l["count"] == 2) || - (l["lootName"] == "CHAMPION_TOKEN_7" && l["count"] == 3) - end - - if loot_mastery_tokens.count > 0 - loot_mastery_tokens = - loot_mastery_tokens.sort_by { |l| [l["lootName"], l["itemDesc"]] } - puts "We could upgrade the following champions:\n".light_blue - needed_shards = 0 - needed_perms = 0 - needed_essence = 0 - - loot_mastery_tokens.each do |t| - ref_shard = - loot_shards.select { |l| t["refId"] == l["storeItemId"].to_s } - ref_perm = - loot_shards.select { |l| t["refId"] == l["storeItemId"].to_s } - - print pad(t["itemDesc"], 15, false).light_white - print " to Mastery Level ".light_black - print "#{(t["lootName"])[-1]}".light_white - print " using ".light_black - if !ref_shard.empty? && ref_shard[0]["count"] > 0 - print "a champion shard.".green - needed_shards += 1 - t["upgrade_type"] = "shard" - elsif !ref_perm.empty? && ref_shard[0]["count"] > 0 - print "a champion permanent.".green - needed_perms += 1 - t["upgrade_type"] = "permanent" - else - recipe_cost = (t["lootName"])[-1] == "6" ? recipe6_cost : recipe7_cost - print "#{recipe_cost} Blue Essence.".yellow - needed_essence += recipe_cost - t["upgrade_type"] = "essence" - end - puts - end - puts - - owned_essence = - player_loot.select { |l| l["lootId"] == "CURRENCY_champion" } - owned_essence = owned_essence[0]["count"] - if (owned_essence > needed_essence) - question_string = - "Upgrade #{loot_mastery_tokens.count} champions using " - question_string += "#{needed_shards} Shards, " if needed_shards > 0 - question_string += "#{needed_perms} Permanents, " if needed_perms > 0 - question_string += - "#{needed_essence} Blue Essence, " if needed_essence > 0 - question_string = question_string.delete_suffix(", ") - question_string += "?" - - if $ans_y.include? user_input_check( - question_string, - $ans_yn, - $ans_yn_d, - "confirm" - ) - loot_mastery_tokens.each do |t| - $s_redeemed += 1 - target_level = (t["lootName"])[-1] - case t["upgrade_type"] - when "shard" - post_recipe( - "CHAMPION_TOKEN_#{target_level}_redeem_withshard", - [t["lootId"], "CHAMPION_RENTAL_#{t["refId"]}"], - 1 - ) - when "permanent" - post_recipe( - "CHAMPION_TOKEN_#{target_level}_redeem_withpermanent", - [t["lootId"], "CHAMPION_#{t["refId"]}"], - 1 - ) - when "essence" - post_recipe( - "CHAMPION_TOKEN_#{target_level}_redeem_withessence", - [t["lootId"], "CURRENCY_champion"], - 1 - ) - end - end - end - else - puts "You're missing #{needed_essence - owned_essence} Blue Essence needed to proceed. Skipping...".yellow - end - else - puts "Found no upgradable set of Mastery Tokens.".yellow - end - rescue => exception - handle_exception(exception, "token upgrades") - end -end - -def handle_generic(name, type, recipe) - begin - player_loot = get_player_loot - disenchant_all = true - - loot_generic = player_loot.select { |l| l["type"] == type } - if count_loot_items(loot_generic) > 0 - puts "Found #{count_loot_items(loot_generic)} #{name}.".light_blue - - contains_unowned_items = false - loot_generic.each do |l| - if l["redeemableStatus"] != "ALREADY_OWNED" - contains_unowned_items = true - end - end - - if contains_unowned_items - user_option = - user_input_check( - "Keep #{name} you don't own yet?\n".light_cyan + - "[y] ".light_white + "Yes\n".light_cyan + "[n] ".light_white + - "No\n".light_cyan + "[x] ".light_white + - "Exit to main menu\n".light_cyan + "Option: ", - %w[y n x], - "[y|n|x]", - "" - ) - - case user_option - when "x" - puts "Action cancelled".yellow - return - when "y" - disenchant_all = false - loot_generic = - loot_generic.select { |g| g["redeemableStatus"] == "ALREADY_OWNED" } - puts "Filtered to #{count_loot_items(loot_generic)} items.".light_blue - end - end - - if count_loot_items(loot_generic) > 0 - total_oe_value = 0 - loot_generic.each do |g| - total_oe_value += g["disenchantValue"] * g["count"] - end - - if loot_generic[0]["itemDesc"] == "" - loot_name_index = "localizedName" - else - loot_name_index = "itemDesc" - end - loot_generic = - loot_generic.sort_by do |l| - [l["redeemableStatus"], l[loot_name_index]] - end - - puts "We'd disenchant #{count_loot_items(loot_generic)} #{name} using the option you chose:".light_blue - loot_generic.each do |l| - loot_value = l["disenchantValue"] * l["count"] - print pad("#{l["count"]}x ", 5, false).light_black - print pad("#{l[loot_name_index]}", 30).light_white - print " @ ".light_black - print pad("#{loot_value} OE", 8, false).light_black - if disenchant_all && l["redeemableStatus"] != "ALREADY_OWNED" - print " (not owned)".yellow - end - puts - end - - if ($ans_y).include? user_input_check( - "Disenchant #{count_loot_items(loot_generic)} #{name} for #{total_oe_value} Orange Essence?", - $ans_yn, - $ans_yn_d, - "confirm" - ) - $s_disenchanted += count_loot_items(loot_generic) - $s_orange_essence += total_oe_value - threads = - loot_generic.map do |g| - Thread.new { post_recipe(recipe, g["lootId"], g["count"]) } - end - threads.each(&:join) - puts "Done!".green - end - else - puts "Found no owned #{name} to disenchant.".yellow - end - else - puts "Found no #{name} to disenchant.".yellow - end - rescue => exception - handle_exception(exception, name) - end -end - -def handle_skins - handle_generic("Skin Shards", "SKIN_RENTAL", "SKIN_RENTAL_disenchant") - handle_generic("Skin Permanents", "SKIN", "SKIN_disenchant") -end - -def handle_eternals - handle_generic( - "Eternal Shards", - "STATSTONE_SHARD", - "STATSTONE_SHARD_DISENCHANT" - ) - handle_generic("Eternals", "STATSTONE", "STATSTONE_DISENCHANT") -end - -def handle_emotes - handle_generic("Emotes", "EMOTE", "EMOTE_disenchant") -end - -def handle_ward_skins - handle_generic( - "Ward Skin Shards", - "WARDSKIN_RENTAL", - "WARDSKIN_RENTAL_disenchant" - ) - handle_generic("Ward Skin Permanents", "WARDSKIN", "WARDSKIN_disenchant") -end - -def handle_icons - handle_generic("Icons", "SUMMONERICON", "SUMMONERICON_disenchant") -end - -def handle_champions - begin - player_loot = get_player_loot - loot_shards = player_loot.select { |l| l["type"] == "CHAMPION_RENTAL" } - - loot_perms = player_loot.select { |l| l["type"] == "CHAMPION" } - if count_loot_items(loot_perms) > 0 - if ($ans_y).include? user_input_check( - "Should we include champion permanents in this process?", - $ans_yn, - $ans_yn_d - ) - loot_shards = - player_loot.select do |l| - l["type"] == "CHAMPION_RENTAL" || l["type"] == "CHAMPION" - end - end - end - - if count_loot_items(loot_shards) > 0 - puts "Found #{count_loot_items(loot_shards)} champion shards.".light_blue - - loot_shards.each do |s| - s["count_keep"] = 0 - s["disenchant_note"] = "" - end - loot_shards_not_owned = - loot_shards.select { |s| s["redeemableStatus"] != "ALREADY_OWNED" } - - if loot_shards_not_owned.length > 0 - if ($ans_y).include? user_input_check( - "Keep a shard for champions you don't own yet?", - $ans_yn, - $ans_yn_d - ) - loot_shards = handle_champions_owned(loot_shards) - end - else - puts "Found no shards of champions you don't own yet.".light_blue - end - - disenchant_modes = { - "1" => "Disenchant all champion shards", - "2" => - "Keep enough (1/2) shards for champions you own mastery 6/7 tokens for", - "3" => - "Keep enough (1/2) shards to fully master champions at least at mastery level x (select from 1 to 6)", - "4" => - "Keep enough (1/2) shards to fully master all champions (only disenchant shards that have no possible use)", - "5" => "Keep one shard of each champion regardless of mastery", - "x" => "Cancel" - } - - modes_string = "" - disenchant_modes.each do |k, v| - modes_string += "[#{k}] ".light_white - modes_string += "#{v}\n".light_cyan - end - - disenchant_shards_mode = - user_input_check( - "Okay, which option would you like to go by?\n" + modes_string + - "Option: ", - disenchant_modes.keys, - "[1|2|3|4|5|x]", - "" - ) - unless disenchant_shards_mode == "x" - case disenchant_shards_mode - when "1" - # no filtering needed -> done - when "2" - loot_shards = handle_champions_tokens(player_loot, loot_shards) - when "3" - loot_shards = handle_champions_mastery(loot_shards) - when "4" - loot_shards = handle_champions_mastery(loot_shards, true) - when "5" - loot_shards = handle_champions_collection(loot_shards) - end - - loot_shards = loot_shards.select { |l| l["count"] > 0 } - loot_shards = - loot_shards.sort_by { |l| [l["disenchant_note"], l["itemDesc"]] } - - if count_loot_items(loot_shards) > 0 - puts "We'd disenchant #{count_loot_items(loot_shards)} champion shards using the option you chose:".light_blue - loot_shards.each do |l| - loot_value = l["disenchantValue"] * l["count"] - print pad("#{l["count"]}x ", 5, false).light_black - print pad("#{l["itemDesc"]}", 15).light_white - print " @ ".light_black - print pad("#{loot_value} BE", 8, false).light_black - if l["count_keep"] > 0 - puts " keeping #{l["count_keep"]}".green - elsif l["disenchant_note"].length > 0 - puts " #{l["disenchant_note"]}" - else - puts - end - end - - loot_shards = handle_champions_exceptions(loot_shards) - - total_be_value = 0 - loot_shards.each do |l| - total_be_value += l["disenchantValue"] * l["count"] - end - - if count_loot_items(loot_shards) > 0 - if $ans_y.include? user_input_check( - "Disenchant #{count_loot_items(loot_shards)} champion shards for #{total_be_value} Blue Essence?", - $ans_yn, - $ans_yn_d, - "confirm" - ) - $s_blue_essence += total_be_value - $s_disenchanted += count_loot_items(loot_shards) - threads = - loot_shards.map do |s| - Thread.new do - post_recipe( - "CHAMPION_RENTAL_disenchant", - s["lootId"], - s["count"] - ) - end - end - threads.each(&:join) - puts "Done!".green - end - else - puts "All remaining champions have been excluded, skipping...".yellow - end - else - puts "Job's already done: no champion shards left matching your selection.".green - end - else - puts "Champion shard disenchanting canceled.".yellow - end - else - puts "Found no champion shards to disenchant.".yellow - end - rescue => exception - handle_exception(exception, "Champion Shards") - end -end - -def handle_champions_owned(loot_shards) - begin - loot_shards.each do |l| - unless l["redeemableStatus"] == "ALREADY_OWNED" - l["count"] -= 1 - l["count_keep"] += 1 - end - end - return loot_shards.select { |l| l["count"] > 0 } - rescue => exception - handle_capsules(exception, "Owned Champion Shards") - end -end - -def handle_champions_tokens(player_loot, loot_shards) - begin - token6_champion_ids = [] - token7_champion_ids = [] - - loot_mastery_tokens = - player_loot.select { |l| l["type"] == "CHAMPION_TOKEN" } - - loot_mastery_tokens.each do |token| - if token["lootName"] = "CHAMPION_TOKEN_6" - token6_champion_ids << token["refId"].to_i - elsif token["lootName"] = "CHAMPION_TOKEN_7" - token7_champion_ids << token["refId"].to_i - end - end - - puts "Found #{token6_champion_ids.length + token7_champion_ids.length} champions with owned mastery tokens".light_black - - loot_shards = - loot_shards.each do |l| - if token6_champion_ids.include? l["storeItemId"] - l["count"] -= 2 - l["count_keep"] += 2 - elsif token7_champion_ids.include? l["storeItemId"] - l["count"] -= 1 - l["count_keep"] += 1 - end - end - return loot_shards - rescue => exception - handle_exception(exception, "Champion Shards by Tokens") - end -end - -def handle_champions_mastery(loot_shards, keep_all = false) - begin - summoner = get_current_summoner - player_mastery = get_champion_mastery(summoner["summonerId"]) - threshold_champion_ids = [] - mastery6_champion_ids = [] - mastery7_champion_ids = [] - - unless keep_all - level_threshold = - user_input_check( - "Which mastery level should champions at least be for their shards to be kept?", - %w[1 2 3 4 5 6], - "[1..6]" - ) - else - level_threshold = "0" - end - level_threshold = level_threshold.to_i - - player_mastery.each do |m| - if m["championLevel"] == 7 - mastery7_champion_ids << m["championId"] - elsif m["championLevel"] == 6 - mastery6_champion_ids << m["championId"] - elsif (level_threshold..5).include? m["championLevel"] - threshold_champion_ids << m["championId"] - elsif keep_all - threshold_champion_ids << m["championId"] - end - end - - loot_shards.each do |l| - if mastery7_champion_ids.include? l["storeItemId"] - l["disenchant_note"] = "at mastery 7".light_black - elsif mastery6_champion_ids.include? l["storeItemId"] - l["count"] -= 1 - l["count_keep"] += 1 - elsif threshold_champion_ids.include? l["storeItemId"] - l["count"] -= 2 - l["count_keep"] += 2 - else - l["disenchant_note"] = "below threshold".yellow - end - end - - return loot_shards - rescue => exception - handle_exception(exception, "Champion Shards by Mastery") - end -end - -def handle_champions_collection(loot_shards) - begin - loot_shards.each do |l| - l["count"] -= 1 - l["count_keep"] += 1 - end - - return loot_shards - rescue => exception - handle_exception(exception, "Champion Shards for Collection") - end -end - -def handle_champions_exceptions(loot_shards) - begin - exclusions_str = "" - exclusions_done = false - exclusions_done_more = "" - exclusions_arr = [] - until exclusions_done - if ($ans_y).include? user_input_check( - "Would you like to add #{exclusions_done_more}exclusions?", - $ans_yn, - $ans_yn_d - ) - exclusions_str += - "," + - ask( - "Okay, which champions? ".light_cyan + - "(case-sensitive, comma-separated)".light_white + - ": ".light_cyan - ) - - exclusions_done_more = "more " - - exclusions_arr = exclusions_str.split(/\s*,\s*/) - exclusions_matched = - loot_shards.select { |l| exclusions_arr.include? l["itemDesc"] } - print "Exclusions recognized: ".green - exclusions_matched.each { |e| print e["itemDesc"].light_white + " " } - puts - else - exclusions_done = true - end - end - loot_shards = - loot_shards.select { |l| !exclusions_arr.include? l["itemDesc"] } - return loot_shards - rescue => exception - handle_exception(exception, "Champion Shard Exceptions") - end -end - -def open_github - puts "Opening GitHub repository at https://github.com/marvinscham/disenchanter/ in your browser...".light_blue - Launchy.open("https://github.com/marvinscham/disenchanter/") -end - -def open_stats - puts "Opening Global Stats at https://github.com/marvinscham/disenchanter/wiki/Stats in your browser...".light_blue - Launchy.open("https://github.com/marvinscham/disenchanter/wiki/Stats") -end - -def handle_debug - done = false - things_todo = { - "1" => "Write player_loot to file", - "2" => "Write recipes of lootId to file", - "3" => "Write loot info of lootId to file", - "m" => "Enable debug mode", - "x" => "Back to main menu" - } - things_done = [] - - until done - todo_string = "" - things_todo.each do |k, v| - todo_string += "[#{k}] ".light_white - unless things_done.include? k - todo_string += "#{v}\n".light_cyan - else - todo_string += "#{v} (done)\n".light_green - end - end - - todo = - user_input_check( - "\nWhat would you like to do?\n\n".light_cyan + todo_string + - "Option: ", - things_todo.keys, - "", - "" - ) - things_done << todo - - puts $sep - puts - - puts "Option chosen: #{things_todo[todo]}".light_white - - case todo - when "1" - player_loot = get_player_loot - - File.open("disenchanter_loot.json", "w") do |f| - f.write(player_loot.to_json) - end - - puts("Okay, written to disenchanter_loot.json.") - when "2" - loot_id = ask("Which lootId would you like the recipes for?\n".light_cyan) - - recipes = get_recipes_for_item loot_id - - File.open("disenchanter_recipes.json", "w") do |f| - f.write(recipes.to_json) - end - - puts("Okay, written to disenchanter_recipes.json.") - when "3" - loot_id = ask("Which lootId would you like the info for?\n".light_cyan) - - loot_info = get_loot_info loot_id - - File.open("disenchanter_lootinfo.json", "w") do |f| - f.write(loot_info.to_json) - end - - puts("Okay, written to disenchanter_lootinfo.json.") - when "m" - $debug = true - puts "Debug mode enabled." - when "x" - done = true - end - puts $sep - end -end - -def handle_stat_submission - if $actions > 0 - strlen = 15 - numlen = 7 - stats_string = "Your stats:\n".light_blue - stats_string += - pad("Actions", strlen) + pad($actions.to_s, numlen, false).light_white + - "\n" - stats_string += - pad("Disenchanted", strlen) + - pad($s_disenchanted.to_s, numlen, false).light_white + "\n" - stats_string += - pad("Opened", strlen) + pad($s_opened.to_s, numlen, false).light_white + - "\n" - stats_string += - pad("Crafted", strlen) + pad($s_crafted.to_s, numlen, false).light_white + - "\n" - stats_string += - pad("Redeemed", strlen) + - pad($s_redeemed.to_s, numlen, false).light_white + "\n" - stats_string += - pad("Blue Essence", strlen) + - pad($s_blue_essence.to_s, numlen, false).light_white + "\n" - stats_string += - pad("Orange Essence", strlen) + - pad($s_orange_essence.to_s, numlen, false).light_white + "\n" - - if ($ans_y).include? user_input_check( - "Would you like to contribute your (anonymous) stats to the global stats?\n".light_cyan + - stats_string + "[y|n]: ", - $ans_yn, - $ans_yn_d, - "" - ) - submit_stats( - $actions, - $s_disenchanted, - $s_opened, - $s_crafted, - $s_redeemed, - $s_blue_essence, - $s_orange_essence - ) - puts "Thank you very much!".light_green - end - end -end - -def submit_stats(a, d, o, c, r, be, oe) - begin - uri = URI("https://checksch.de/hook/disenchanter.php") - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - req = Net::HTTP::Post.new(uri, "Content-Type": "application/json") - - req.body = { a: a, d: d, o: o, c: c, r: r, be: be, oe: oe }.to_json - http.request(req) - rescue => exception - handle_exception(exception, "stat submission") - end -end - -run diff --git a/disenchanter_up.rb b/disenchanter_up.rb deleted file mode 100644 index c03b971..0000000 --- a/disenchanter_up.rb +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env ruby -# frozen_string_literal: true - -require "net/https" -require "base64" -require "json" -require "colorize" -require "open-uri" - -puts "Grabbing latest version of Disenchanter...".light_blue - -def run - sep = - "____________________________________________________________".light_black - - if File.exist?("LeagueClient.exe") - # Doinb 400CS backwards compatibility hack - updater_processes = `tasklist | find /I /C "disenchanter_up.exe"` - if(updater_processes.to_i > 2) - puts "Backwards compatibility: killing Disenchanter..." - `taskkill /IM "disenchanter.exe" /F /T >nul 2>&1 && exit` - end - `tasklist|findstr "disenchanter.exe" >nul 2>&1 && echo Backwards compatibility: popping out into separate process... && start cmd.exe @cmd /k "disenchanter_up.exe" && exit` - sleep(1) - puts "Killing Disenchanter..." - `taskkill /IM "disenchanter.exe" /F /T >nul 2>&1` - - uri = - URI( - "https://api.github.com/repos/marvinscham/disenchanter/releases/latest" - ) - http = Net::HTTP.new(uri.host, uri.port) - http.use_ssl = true - http.verify_mode = OpenSSL::SSL::VERIFY_NONE - req = Net::HTTP::Get.new(uri, "Content-Type": "application/json") - res = http.request req - ans = JSON.parse(res.body) - - puts "Downloading Disenchanter #{ans["tag_name"]}".light_green - - `curl https://github.com/marvinscham/disenchanter/releases/download/#{ans["tag_name"]}/disenchanter.exe -L -o disenchanter.exe` - puts sep - puts "Done downloading!".green - - pid = spawn("start cmd.exe @cmd /k \"disenchanter.exe\"") - Process.detach(pid) - puts "Exiting...".light_black - exit - else - puts "Not in League Client folder, skipping update...".yellow - end -end - -run diff --git a/scripts/build_main.sh b/scripts/build_main.sh new file mode 100644 index 0000000..c9f68b7 --- /dev/null +++ b/scripts/build_main.sh @@ -0,0 +1,10 @@ +#!/bin/bash +mkdir -p build +touch ./build/.build.lockfile + +ocran src/main.rb \ + --gemfile ./Gemfile \ + --icon ./assets/BE_icon.ico \ + --output ./build/disenchanter.exe + +rm ./build/.build.lockfile diff --git a/scripts/build_updater.sh b/scripts/build_updater.sh new file mode 100644 index 0000000..af8d6b2 --- /dev/null +++ b/scripts/build_updater.sh @@ -0,0 +1,10 @@ +#!/bin/bash +mkdir -p build +touch ./build/.build.lockfile + +ocran src/updater.rb \ + --gemfile ./Gemfile \ + --icon ./assets/BE_icon.ico \ + --output ./build/disenchanter_up.exe + +rm ./build/.build.lockfile diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..feb5804 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,9 @@ +sonar.projectKey=marvinscham_disenchanter_AY088C575jK4ghmshIlp +sonar.projectVersion=v1.6.0 + +sonar.sources=. +sonar.sourceEncoding=UTF-8 + +sonar.coverage.exclusions=**Test.php,**test.php,**.test.js,pages/** + +sonar.ruby.rubocop.reportPaths=rubocop-report.json diff --git a/src/class/client.rb b/src/class/client.rb new file mode 100644 index 0000000..e1a648a --- /dev/null +++ b/src/class/client.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'base64' +require 'net/http' +require 'json' + +require_relative '../modules/detect_client' + +# Holds port and token info +class Client + attr_accessor :stat_tracker, :debug, :dry_run + + # @param stat_tracker StatTracker + def initialize(stat_tracker) + begin + @port, @token = grab_lockfile + rescue StandardError + ask exit_string + exit 1 + end + @stat_tracker = stat_tracker + @debug = false + @dry_run = false + end + + def host + "https://127.0.0.1:#{@port}" + end + + def auth + "Basic #{@token.chomp}" + end + + def create_client(&) + Net::HTTP.start( + '127.0.0.1', + @port, + use_ssl: true, + verify_mode: OpenSSL::SSL::VERIFY_NONE, & + ) + end + + def req_set_headers(req) + req['Content-Type'] = 'application/json' + req['Authorization'] = auth + end + + def request_get(path) + create_client do |http| + uri = URI("#{host}/#{path}") + req = Net::HTTP::Get.new(uri) + req_set_headers(req) + res = http.request req + JSON.parse(res.body) + end + end + + def request_post(path, body) + puts "Posting against #{host}/#{path}".light_black if @debug + puts 'DRY RUN ENABLED - actually did nothing'.light_red if @dry_run + return if @dry_run + + create_client do |http| + uri = URI("#{host}/#{path}") + req = Net::HTTP::Post.new(uri, 'Content-Type': 'application/json') + req.body = body + req_set_headers(req) + res = http.request req + JSON.parse(res.body) + end + end + + def refresh_loot + request_post('lol-loot/v1/refresh?force=true', '') + end + + def req_get_current_summoner + request_get('lol-summoner/v1/current-summoner') + end + + def req_get_player_loot + request_get('lol-loot/v1/player-loot') + end + + def req_get_champion_mastery(summoner_id) + request_get("lol-collections/v1/inventories/#{summoner_id}/champion-mastery") + end + + def req_get_loot_info(loot_id) + request_get("lol-loot/v1/player-loot/#{loot_id}") + end + + def req_get_recipes_for_item(loot_id) + request_get("lol-loot/v1/recipes/initial-item/#{loot_id}") + end + + def req_post_recipe(recipe, loot_ids, repeat) + @stat_tracker.add_actions(repeat) + + loot_id_string = "[\"#{Array(loot_ids).join('", "')}\"]" + + post_answer = + request_post( + "lol-loot/v1/recipes/#{recipe}/craft?repeat=#{repeat}", + loot_id_string + ) + handle_post_debug(post_answer) + post_answer + end + + def handle_post_debug(post_answer) + return unless @debug + + File.write('disenchanter_post.json', post_answer.to_json) + puts('Okay, written to disenchanter_post.json.') + end +end diff --git a/src/class/dictionary.rb b/src/class/dictionary.rb new file mode 100644 index 0000000..d375b89 --- /dev/null +++ b/src/class/dictionary.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# Holds constants for Loot IDs +class Dictionary + BLUE_ESSENCE = 'CURRENCY_champion' + ORANGE_ESSENCE = 'CURRENCY_cosmetic' + MYTHIC_ESSENCE = 'CURRENCY_mythic' + + CHAMPION_SHARD = 'CHAMPION_RENTAL' + CHAMPION_PERMANENT = 'CHAMPION' + + MASTERY_6_TOKEN = 'CHAMPION_TOKEN_6' + MASTERY_7_TOKEN = 'CHAMPION_TOKEN_7' + + MASTERY_6_RECIPE = 'CHAMPION_TOKEN_6_redeem_withessence' + MASTERY_7_RECIPE = 'CHAMPION_TOKEN_7_redeem_withessence' + + RANDOM_SKIN_SHARD = 'CHEST_291' +end diff --git a/src/class/menu/champions_menu.rb b/src/class/menu/champions_menu.rb new file mode 100644 index 0000000..db6e060 --- /dev/null +++ b/src/class/menu/champions_menu.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative 'menu' + +require_relative '../../modules/handlers/champions_collection' +require_relative '../../modules/handlers/champions_exclusions' +require_relative '../../modules/handlers/champions_mastery' +require_relative '../../modules/handlers/champions_owned' +require_relative '../../modules/handlers/champions_tokens' + +# Menu to select and call filtering options +class ChampionsMenu < Menu + attr_reader :loot_shards + + def initialize(client, loot_shards) + menu_text = 'Okay, which option would you like to go by?' + things_todo = { + '1' => 'Disenchant all champion shards', + '2' => 'Keep enough (1/2) shards for champions you own mastery 6/7 tokens for', + '3' => 'Keep enough (1/2) shards to fully master champions at least at mastery level x (select from 1 to 6)', + '4' => 'Keep enough (1/2) shards to fully master all champions ' \ + '(only disenchant shards that have no possible use)', + '5' => 'Keep one shard of each champion regardless of mastery', + 'x' => 'Back to main menu' + } + answer_display = 'Option' + + super(client, menu_text, answer_display, things_todo) + @loot_shards = loot_shards + end + + # Calls the corresponding champion shard handling method + # @param thing_todo Option name + # @return true if done + def handle_option(thing_todo) + case thing_todo + when '1', 'x' + # no filtering needed -> done + when '2' + @loot_shards = handle_champions_tokens(@client, @loot_shards) + when '3' + @loot_shards = handle_champions_mastery(@client, @loot_shards) + when '4' + @loot_shards = handle_champions_mastery(@client, @loot_shards, keep_all: true) + when '5' + @loot_shards = handle_champions_collection(@loot_shards) + else + return false + end + + @loot_shards = @loot_shards.select { |l| l['count'].positive? } + @loot_shards = @loot_shards.sort_by { |l| [l['disenchant_note'], l['itemDesc']] } + + true + end +end diff --git a/src/class/menu/debug_menu.rb b/src/class/menu/debug_menu.rb new file mode 100644 index 0000000..9d4399a --- /dev/null +++ b/src/class/menu/debug_menu.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require_relative 'menu' + +# An interactive debug menu +class DebugMenu < Menu + def initialize(client) + menu_text = 'What would you like to do?' + things_todo = { + '1' => 'Write player_loot to file', + '2' => 'Write recipes of lootId to file', + '3' => 'Write loot info of lootId to file', + '4' => 'Write summoner info to file', + 'd' => 'Toggle dry run', + 'm' => 'Toggle debug mode', + 'x' => 'Back to main menu' + } + answer_display = 'Option' + + super(client, menu_text, answer_display, things_todo) + end + + # Handles the debug step the user selected + # @param thing_todo Option name + # @return true if done + def handle_option(thing_todo) + case thing_todo + when '1' + debug_save_player_loot(@client) + when '2' + debug_save_recipe(@client) + when '3' + debug_save_loot_info(@client) + when '4' + debug_save_summoner_info(@client) + when 'd' + @client.dry_run = !@client.dry_run + @client.debug = @client.dry_run + puts @client.dry_run ? 'Dry run + debug enabled' : 'Dry run + debug disabled' + when 'm' + @client.debug = !@client.debug + puts @client.debug ? 'Debug mode enabled' : 'Debug mode disabled' + when 'x' + return true + else + return false + end + + false + end +end diff --git a/src/class/menu/main_menu.rb b/src/class/menu/main_menu.rb new file mode 100644 index 0000000..a99e2d0 --- /dev/null +++ b/src/class/menu/main_menu.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require_relative 'menu' + +require_relative '../../modules/handlers/champions' +require_relative '../../modules/handlers/emotes' +require_relative '../../modules/handlers/eternals' +require_relative '../../modules/handlers/exception' +require_relative '../../modules/handlers/icons' +require_relative '../../modules/handlers/skins' +require_relative '../../modules/handlers/tacticians' +require_relative '../../modules/handlers/wards' +require_relative '../../modules/handlers/debug' +require_relative '../../modules/handlers/materials' + +require_relative '../../modules/open_url' +require_relative '../../modules/stat_submission' + +# The main menu +class MainMenu < Menu + def initialize(client) + menu_text = 'What would you like to do? (Hint: go top to bottom so you don\'t miss anything!)' + things_todo = { + '1' => 'Materials', + '2' => 'Champions', + '3' => 'Skins', + '4' => 'Tacticians', + '5' => 'Eternals', + '6' => 'Emotes', + '7' => 'Ward Skins', + '8' => 'Icons', + 'm' => 'Open Mastery Chart profile', + 's' => 'Open Disenchanter Global Stats', + 'r' => 'Open GitHub repository', + 'd' => 'Debug Tools', + 'x' => 'Exit' + } + answer_display = 'Option' + + super(client, menu_text, answer_display, things_todo) + end + + def handle_option(todo) + case todo + when '1' + handle_materials(@client) + when '2' + handle_champions(@client) + when '3' + handle_skins(@client) + when '4' + handle_tacticians(@client) + when '5' + handle_eternals(@client) + when '6' + handle_emotes(@client) + when '7' + handle_ward_skins(@client) + when '8' + handle_icons(@client) + when 'm' + open_masterychart(@client) + when 's' + open_stats + when 'r' + open_github + when 'd' + handle_debug(@client) + when 'x' + return true + else + return false + end + + @client.refresh_loot + puts separator + false + end +end diff --git a/src/class/menu/materials_menu.rb b/src/class/menu/materials_menu.rb new file mode 100644 index 0000000..36ebe71 --- /dev/null +++ b/src/class/menu/materials_menu.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative 'menu' + +require_relative '../../modules/handlers/mythic_essence' +require_relative '../../modules/handlers/key_fragments' +require_relative '../../modules/handlers/capsules' +require_relative '../../modules/handlers/mastery_tokens' + +# Menu for handling materials such as fragments, mythic essence or capsules +class MaterialsMenu < Menu + def initialize(client) + menu_text = 'What would you like to do?' + things_todo = { + '1' => 'Mythic Essence', + '2' => 'Key Fragments', + '3' => 'Capsules', + '4' => 'Mastery Tokens', + 'x' => 'Back to main menu' + } + answer_display = 'Option' + + super(client, menu_text, answer_display, things_todo) + end + + # Calls the corresponding material handling method + # @param thing_todo Option name + # @return true if done + def handle_option(thing_todo) + case thing_todo + when '1' + handle_mythic_essence(@client) + when '2' + handle_key_fragments(@client) + when '3' + handle_capsules(@client) + when '4' + handle_mastery_tokens(@client) + when 'x' + return true + else + return false + end + + false + end +end diff --git a/src/class/menu/menu.rb b/src/class/menu/menu.rb new file mode 100644 index 0000000..e387c94 --- /dev/null +++ b/src/class/menu/menu.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +# An interactive terminal menu +class Menu + attr_reader :things_todo + + # @param client Client connector + # @param menu_text Text presented at the menu's top + # @param answer_display Display text for answers + # @param things_todo Dict of selectable options + def initialize(client, menu_text, answer_display, things_todo) + @client = client + @menu_text = menu_text + @answer_display = answer_display + @things_todo = things_todo + + @things_done = [] + end + + # Runs a menu loop until it's either done or the user bails to the main menu + # @return true if user bailed + def run_loop + done = false + bail = false + + until done || bail + thing_todo = user_input_check( + "\n#{@menu_text}\n\n".light_cyan + todo_str, + @things_todo.keys, + @answer_display, + @client.dry_run ? 'dry' : 'default' + ) + @things_done << thing_todo + puts separator + "\n\nOption chosen: #{@things_todo[thing_todo]}".light_white + + done = handle_option(thing_todo) + bail = thing_todo == 'x' + + puts separator + end + + bail + end + + def todo_str + todo_string = '' + @things_todo.each do |k, v| + todo_string += "[#{k}] ".light_white + todo_string += if @things_done.include? k + "#{v} (done)\n".light_green + else + "#{v}\n".light_cyan + end + end + + todo_string + end + + # Override this! + def handle_option(thing_todo) + puts "Stump for option: #{thing_todo}." + end +end diff --git a/src/class/menu/mythic_menu.rb b/src/class/menu/mythic_menu.rb new file mode 100644 index 0000000..c40cab3 --- /dev/null +++ b/src/class/menu/mythic_menu.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative 'menu' + +require_relative '../dictionary' + +# Menu to handle what to do with mythic essence +class MythicMenu < Menu + attr_reader :thing_todo, :recipe + + def initialize(client) + menu_text = 'Okay, what would you like to craft?' + things_todo = { + '1' => 'Blue Essence', + '2' => 'Orange Essence', + '3' => 'Random Skin Shards', + 'x' => 'Back to main menu' + } + answer_display = 'Option' + + super(client, menu_text, answer_display, things_todo) + @thing_todo = '' + @recipe = '' + end + + def handle_option(thing_todo) + @thing_todo = thing_todo + + case thing_todo + when '1' + thing_to_craft = Dictionary::BLUE_ESSENCE + when '2' + thing_to_craft = Dictionary::ORANGE_ESSENCE + when '3' + thing_to_craft = Dictionary::RANDOM_SKIN_SHARD + when 'x' + return true + else + return false + end + + recipes = @client.req_get_recipes_for_item(Dictionary::MYTHIC_ESSENCE) + recipes = recipes.select { |r| r['outputs'][0]['lootName'] == thing_to_craft } + + if recipes.empty? + puts "Recipes for #{@things_todo[@thing_todo]} seem to be unavailable.".yellow + return + end + @recipe = recipes[0] + + puts "Recipe found: #{@recipe['contextMenuText']} for " \ + "#{@recipe['slots'][0]['quantity']} Mythic Essence".light_blue + + true + end +end diff --git a/src/class/stat_tracker.rb b/src/class/stat_tracker.rb new file mode 100644 index 0000000..0bbfe48 --- /dev/null +++ b/src/class/stat_tracker.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Tracks usage stats for later optional submission +class StatTracker + def initialize + @actions = 0 + @blue_essence = 0 + @orange_essence = 0 + @disenchanted = 0 + @crafted = 0 + @redeemed = 0 + @opened = 0 + end + + attr_reader :actions, :blue_essence, :orange_essence, :disenchanted, :crafted, :redeemed, :opened + + def add_blue_essence(count) + @blue_essence += count + end + + def add_orange_essence(count) + @orange_essence += count + end + + def add_disenchanted(count) + @disenchanted += count + end + + def add_actions(count) + @actions += count + end + + def add_crafted(count) + @crafted += count + end + + def add_redeemed(count) + @redeemed += count + end + + def add_opened(count) + @opened += count + end +end diff --git a/src/main.rb b/src/main.rb new file mode 100644 index 0000000..8eb3eff --- /dev/null +++ b/src/main.rb @@ -0,0 +1,72 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require_relative 'class/client' +require_relative 'class/menu/main_menu' +require_relative 'class/stat_tracker' + +require_relative 'modules/common_strings' +require_relative 'modules/user_input' + +require_relative 'modules/update/checker' + +def run + check_build_env + + current_version = 'v1.6.0' + stat_tracker = StatTracker.new + client = Client.new(stat_tracker) + + greet(client, current_version) + + MainMenu.new(client).run_loop + + finish(stat_tracker) +end + +def greet(client, current_version) + puts 'Hi! :)'.light_green + puts "Running Disenchanter #{current_version}".light_blue + check_update(current_version) + print 'You can exit this script at any point by pressing '.light_blue + puts '[CTRL + C]'.light_white + '.'.light_blue + puts separator + + check_summoner(client) +end + +def check_summoner(client) + summoner = client.req_get_current_summoner + if summoner['gameName'].nil? || summoner['gameName'].empty? + puts 'Could not grab summoner info. Try restarting your League Client.'.light_red + ask exit_string + exit 1 + end + + puts "\nYou're logged in as #{summoner['gameName']} ##{summoner['tagLine']}.".light_blue + puts separator + puts "\nYour loot is safe, no actions will be taken until you confirm a banner like this:".light_blue + puts 'CONFIRM: Perform this action? [y|n]'.light_magenta + puts separator +end + +def finish(stat_tracker) + puts "That's it!".light_green + if stat_tracker.actions.positive? + puts "We saved you about #{stat_tracker.actions * 3} seconds of waiting for animations to finish.".light_green + puts separator + end + handle_stat_submission(stat_tracker) + puts 'See you next time :)'.light_green + ask exit_string +end + +def check_build_env + return unless File.exist?('./build/.build.lockfile') + + puts 'Detected build environment, skipping execution...'.light_yellow + sleep 1 + exit +end + +run diff --git a/src/modules/common_strings.rb b/src/modules/common_strings.rb new file mode 100644 index 0000000..07f7ffa --- /dev/null +++ b/src/modules/common_strings.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'colorize' + +def exit_string + 'Press Enter to exit.'.cyan +end + +def separator + '____________________________________________________________'.light_black +end + +def ans_yn + %w[y yes n no] +end + +def ans_y + %w[y yes] +end + +def ans_n + %w[n no] +end + +def ans_yn_d + '[y|n]' +end + +def pad(str, len, right: true) + format("%#{right ? '-' : ''}#{len}s", str) +end + +unless String.method_defined?(:light_yellow) + # Type hints! + class String + def light_yellow = self + def light_blue = self + def light_green = self + def light_white = self + def light_black = self + def light_red = self + def light_magenta = self + def light_cyan = self + end +end diff --git a/src/modules/detect_client.rb b/src/modules/detect_client.rb new file mode 100644 index 0000000..3dcdffa --- /dev/null +++ b/src/modules/detect_client.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +win_ident = /mswin|mingw|cygwin/ +require 'win32/registry' if RbConfig::CONFIG['host_os'] =~ win_ident +require 'win32/shortcut' if RbConfig::CONFIG['host_os'] =~ win_ident + +# rubocop:disable Style/MixinUsage +include Win32 if RbConfig::CONFIG['host_os'] =~ win_ident +# rubocop:enable Style/MixinUsage + +# rubocop:disable Style/ClassAndModuleChildren +if RbConfig::CONFIG['host_os'] =~ win_ident + module Win32::Registry::Constants + KEY_WOW64_64KEY = 0x0100 + KEY_WOW64_32KEY = 0x0200 + end +end +# rubocop:enable Style/ClassAndModuleChildren + +def grab_lockfile + is_windows = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/).zero? + + if is_windows + credentials = try_grab_lockfile_registry + credentials = try_grab_lockfile_start_menu if credentials == '' + end + + credentials = try_grab_lockfile_default_path if credentials == '' + credentials = try_grab_lockfile_locally if credentials == '' + + _leagueclient, _unk_port, port, password = credentials.split(':') + token = Base64.encode64("riot:#{password.chomp}") + + [port, token] +end + +def try_grab_lockfile_registry + keyname = 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\Riot Game league_of_legends.live' + reg = Win32::Registry::HKEY_CURRENT_USER.open( + keyname, + Win32::Registry::KEY_READ | Win32::Registry::KEY_WOW64_32KEY + ) + + credentials = File.read("#{reg['InstallLocation']}/lockfile") + puts 'Found client via registry'.light_black + + credentials +rescue StandardError + # Just keep going + '' +end + +def try_grab_lockfile_start_menu + sc = Shortcut.open( + 'C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\Riot Games\\League of Legends.lnk' + ) + scpath = sc.path.split('\\') + path = scpath[0..-3].join('\\') + + credentials = File.read("#{path}\\League of Legends\\lockfile") + puts 'Found client via start menu'.light_black + + credentials +rescue StandardError + # Just keep going + '' +end + +def try_grab_lockfile_default_path + credentials = File.read("C:\\Riot Games\\League of Legends\\#{lockfile}") + puts 'Found client at standard path'.light_black + + credentials +rescue StandardError + # Just keep going + '' +end + +def try_grab_lockfile_locally + credentials = File.read(lockfile) + puts 'Found client locally'.light_black + + credentials +rescue StandardError + puts 'Failed to automatically find your League Client.'.light_red + puts 'Make sure your client is running and logged into your account.'.light_red + puts 'If it\'s running and you\'re seeing this, ' \ + 'please place the script directly in your League Client folder.'.light_red +end diff --git a/src/modules/handlers/capsules.rb b/src/modules/handlers/capsules.rb new file mode 100644 index 0000000..3cbe651 --- /dev/null +++ b/src/modules/handlers/capsules.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require_relative '../loot_metainfo' + +# Opens keyless chests/capsules +# @param client Client connector +def handle_capsules(client) + player_loot = client.req_get_player_loot + + loot_capsules = filter_key_capsules(client, player_loot) + + if count_loot_items(loot_capsules).zero? + puts 'Found no keyless capsules to open.'.yellow + return + end + + print_capsule_summary(client, loot_capsules) + + if ans_y.include? user_input_check( + "Open #{count_loot_items(loot_capsules)} (keyless) capsules?", + ans_yn, ans_yn_d, 'confirm' + ) + process_keyless_capsule_requests(loot_capsules, client) + puts 'Done!'.green + end +rescue StandardError => e + handle_exception(e, 'Capsules') +end + +def filter_key_capsules(client, player_loot) + loot_capsules = player_loot.select { |l| l['lootName'].start_with?('CHEST_') } + + loot_capsules.each do |c| + recipes = client.req_get_recipes_for_item(c['lootId']) + c['needs_key'] = recipes[0]['slots'].length > 1 || !recipes[0]['type'] == 'OPEN' + end + + loot_capsules.reject { |c| c['needs_key'] } +rescue StandardError => e + handle_exception(e, 'Capsules: filtering loot') +end + +def process_keyless_capsule_requests(loot_capsules, client) + threads = + loot_capsules.map do |c| + Thread.new do + res = client.req_post_recipe("#{c['lootId']}_OPEN", c['lootId'], c['count']) + unless res.nil? + res['added'].each do |r| + client.stat_tracker.add_blue_essence(r['deltaCount']) if r['playerLoot']['lootId'] == 'CURRENCY_champion' + end + end + end + end + threads.each(&:join) + + client.stat_tracker.add_opened(count_loot_items(loot_capsules)) +rescue StandardError => e + handle_exception(e, 'Capsules: request execution') +end + +def print_capsule_summary(client, loot_capsules) + puts "Found #{count_loot_items(loot_capsules)} capsules:".light_blue + loot_capsules.each do |c| + puts "#{c['count']}x ".light_black + get_chest_name(client, c['lootId']).light_white + end +rescue StandardError => e + handle_exception(e, 'Capsules: summary generation') +end diff --git a/src/modules/handlers/champions.rb b/src/modules/handlers/champions.rb new file mode 100644 index 0000000..d56f736 --- /dev/null +++ b/src/modules/handlers/champions.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require_relative '../../class/menu/champions_menu' + +require_relative '../loot_metainfo' + +require_relative 'champions_collection' +require_relative 'champions_exclusions' +require_relative 'champions_mastery' +require_relative 'champions_owned' +require_relative 'champions_tokens' + +# Handles any champion shard and champion permanent related loot actions +# @param client Client connector +def handle_champions(client) + loot_shards = init_champion_shard_selection(client) + + if count_loot_items(loot_shards).zero? + puts 'Found no champion shards to disenchant.'.yellow + return + end + + puts "Found #{count_loot_items(loot_shards)} champion shards.".light_blue + loot_shards = handle_champions_owned(loot_shards) + + champions_menu = ChampionsMenu.new(client, loot_shards) + bail = champions_menu.run_loop + return if bail + + loot_shards = champions_menu.loot_shards + + if count_loot_items(loot_shards).zero? + puts "Job's already done: no champion shards left matching your selection.".green + return + end + + present_champion_selection(loot_shards) + + pre_exclusion_count = count_loot_items(loot_shards) + loot_shards = handle_champions_exclusions(loot_shards) + + return if count_loot_items(loot_shards).zero? + + present_champion_selection(loot_shards) if pre_exclusion_count != count_loot_items(loot_shards) + + execute_champions_disenchant(client, loot_shards) + puts 'Done!'.green +rescue StandardError => e + handle_exception(e, 'Champion Shards') +end + +def present_champion_selection(loot_shards) + puts "We'd disenchant #{count_loot_items(loot_shards)} champion shards using the option you chose:".light_blue + loot_shards.each do |l| + loot_value = l['disenchantValue'] * l['count'] + print pad("#{l['count']}x ", 5, right: false).light_black + print pad(l['itemDesc'], 15).light_white + print ' @ '.light_black + print pad("#{loot_value} BE", 8, right: false).light_black + print_champion_disenchant_addendum(l) + puts + end +end + +def print_champion_disenchant_addendum(shard) + if shard['count_keep'].positive? + print " keeping #{shard['count_keep']}".green + elsif shard['disenchant_note'].length.positive? + print " #{shard['disenchant_note']}" + end +end + +def init_champion_shard_selection(client) + player_loot = client.req_get_player_loot + loot_shards = player_loot.select { |l| l['type'] == Dictionary::CHAMPION_SHARD } + + loot_perms = player_loot.select { |l| l['type'] == Dictionary::CHAMPION_PERMANENT } + if count_loot_items(loot_perms).positive? && (ans_y.include? user_input_check( + 'Should we include champion permanents in this process?', + ans_yn, + ans_yn_d + )) + loot_shards.concat(loot_perms) + end + + loot_shards +end + +def execute_champions_disenchant(client, loot_shards) + total_be_value = 0 + loot_shards.each do |l| + total_be_value += l['disenchantValue'] * l['count'] + end + + if ans_y.include? user_input_check( + "Disenchant #{count_loot_items(loot_shards)} champion shards for #{total_be_value} Blue Essence?", + ans_yn, + ans_yn_d, + 'confirm' + ) + client.stat_tracker.add_blue_essence(total_be_value) + client.stat_tracker.add_disenchanted(count_loot_items(loot_shards)) + threads = + loot_shards.map do |s| + Thread.new do + client.req_post_recipe( + s['disenchantRecipeName'], + s['lootId'], + s['count'] + ) + end + end + threads.each(&:join) + end +end diff --git a/src/modules/handlers/champions_collection.rb b/src/modules/handlers/champions_collection.rb new file mode 100644 index 0000000..c403ec4 --- /dev/null +++ b/src/modules/handlers/champions_collection.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Will keep one shard for each champion +def handle_champions_collection(loot_shards) + loot_shards.each do |l| + l['count'] -= 1 + l['count_keep'] += 1 + end + + loot_shards +rescue StandardError => e + handle_exception(e, 'Champion Shards for Collection') +end diff --git a/src/modules/handlers/champions_exclusions.rb b/src/modules/handlers/champions_exclusions.rb new file mode 100644 index 0000000..2c2780b --- /dev/null +++ b/src/modules/handlers/champions_exclusions.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Allows adding exceptions to a previously made Champion Shard disenchantment selection +# @param loot_shards Loot array, champion shards and permanents only +def handle_champions_exclusions(loot_shards) + exclusions_str = '' + exclusions_arr = [] + exclusions_done = false + + until exclusions_done + if ans_y.include? user_input_check( + 'Would you like to add exclusions?', + ans_yn, + ans_yn_d + ) + exclusions_arr += handle_champion_exclusion(loot_shards, exclusions_str) + else + exclusions_done = true + end + end + + loot_shards.reject { |l| exclusions_arr.include? l['itemDesc'] } +rescue StandardError => e + handle_exception(e, 'Champion Shard Exceptions') +end + +def handle_champion_exclusion(loot_shards, exclusions_str) + exclusions_str += ',' + exclusions_str += ask( + 'Okay, which champions? '.light_cyan + + '(case-sensitive, comma-separated)'.light_white + + ': '.light_cyan + ) + + exclusions_arr = exclusions_str.split(/\s*,\s*/) + exclusions_matched = loot_shards.select { |l| exclusions_arr.include? l['itemDesc'] } + + print 'Exclusions recognized: '.green + exclusions_matched.each { |e| print "#{e['itemDesc'].light_white} " } + puts + + exclusions_arr +end diff --git a/src/modules/handlers/champions_mastery.rb b/src/modules/handlers/champions_mastery.rb new file mode 100644 index 0000000..3fa7212 --- /dev/null +++ b/src/modules/handlers/champions_mastery.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# Will keep shards to max out champions above a user-specified mastery level +# @param client Client connector +# @param loot_shards Loot array, pre-filtered to only champion shards and permanents +# @param keep_all Whether to keep all shards that could possibly be used for non-collection purposes +def handle_champions_mastery(client, loot_shards, keep_all: false) + summoner = client.req_get_current_summoner + player_mastery = client.req_get_champion_mastery(summoner['summonerId']) + threshold_champion_ids = [] + mastery6_champion_ids = [] + mastery7_champion_ids = [] + level_threshold = 0 + + unless keep_all + level_threshold = user_input_check( + 'Which mastery level should champions at least be for their shards to be kept?', + %w[1 2 3 4 5 6], + '[1..6]' + ).to_i + end + + player_mastery.each do |m| + case m['championLevel'] + when 7 + mastery7_champion_ids << m['championId'] + when 6 + mastery6_champion_ids << m['championId'] + when level_threshold..5 + threshold_champion_ids << m['championId'] + else + # Nothing to do + end + end + + loot_shards.each do |l| + adjust_shard_counts_by_threshold(l, keep_all, mastery6_champion_ids, mastery7_champion_ids, threshold_champion_ids) + end + + loot_shards +rescue StandardError => e + handle_exception(e, 'Champion Shards by Mastery') +end + +def adjust_shard_counts_by_threshold(shard, keep_all, m6_ids, m7_ids, threshold_ids) + if m7_ids.include? shard['storeItemId'] + shard['disenchant_note'] = 'at mastery 7'.light_black + elsif m6_ids.include? shard['storeItemId'] + shard['count'] -= 1 + shard['count_keep'] += 1 + elsif keep_all || (threshold_ids.include? shard['storeItemId']) + shard['count'] -= 2 + shard['count_keep'] += 2 + else + shard['disenchant_note'] = 'below threshold'.yellow + end +end diff --git a/src/modules/handlers/champions_owned.rb b/src/modules/handlers/champions_owned.rb new file mode 100644 index 0000000..23be24a --- /dev/null +++ b/src/modules/handlers/champions_owned.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# Keeps a shard for each champion not owned yet +# @param loot_shards Loot array, pre-filtered to only champion shards and permanents +def handle_champions_owned(loot_shards) + loot_shards.each do |s| + s['count_keep'] = 0 + s['disenchant_note'] = '' + end + loot_shards_not_owned = loot_shards.reject { |s| s['redeemableStatus'] == 'ALREADY_OWNED' } + + if loot_shards_not_owned.empty? + puts "Found no shards of champions you don't own yet.".light_blue + elsif ans_y.include? user_input_check( + "Keep a shard for champions you don't own yet?", + ans_yn, + ans_yn_d + ) + loot_shards.each do |l| + unless l['redeemableStatus'] == 'ALREADY_OWNED' + l['count'] -= 1 + l['count_keep'] += 1 + end + end + end + + loot_shards +rescue StandardError => e + handle_exception(e, 'Owned Champion Shards') +end diff --git a/src/modules/handlers/champions_tokens.rb b/src/modules/handlers/champions_tokens.rb new file mode 100644 index 0000000..66016ed --- /dev/null +++ b/src/modules/handlers/champions_tokens.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# Keeps only shards to max out champions with owned mastery 6/7 tokens +# @param client Client connector +# @param loot_shards Loot array pre-filtered to only champion shards and permanents +def handle_champions_tokens(client, loot_shards) + token6_champion_ids = [] + token7_champion_ids = [] + player_loot = client.req_get_player_loot + + loot_mastery_tokens = + player_loot.select { |l| l['type'] == 'CHAMPION_TOKEN' } + + loot_mastery_tokens.each do |token| + if token['lootName'] == 'CHAMPION_TOKEN_6' + token6_champion_ids << token['refId'].to_i + elsif token['lootName'] == 'CHAMPION_TOKEN_7' + token7_champion_ids << token['refId'].to_i + end + end + + token_champion_count = token6_champion_ids.length + token7_champion_ids.length + puts "Found #{token_champion_count} champions with owned mastery tokens".light_black + + adjust_token_counts(loot_shards, token6_champion_ids, token7_champion_ids) +rescue StandardError => e + handle_exception(e, 'Champion Shards by Tokens') +end + +def adjust_token_counts(loot_shards, token6_champion_ids, token7_champion_ids) + loot_shards.each do |l| + if token6_champion_ids.include? l['storeItemId'] + l['count'] -= 2 + l['count_keep'] += 2 + elsif token7_champion_ids.include? l['storeItemId'] + l['count'] -= 1 + l['count_keep'] += 1 + end + end +end diff --git a/src/modules/handlers/debug.rb b/src/modules/handlers/debug.rb new file mode 100644 index 0000000..42225b7 --- /dev/null +++ b/src/modules/handlers/debug.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require_relative '../../class/menu/debug_menu' + +# Wrapper for debug +# @param client Client connector +def handle_debug(client) + DebugMenu.new(client).run_loop +end + +def debug_save_player_loot(client) + player_loot = client.req_get_player_loot + File.write('disenchanter_loot.json', player_loot.to_json) + puts('Okay, written to disenchanter_loot.json.') +end + +def debug_save_recipe(client) + loot_id = ask("Which lootId would you like the recipes for?\n".light_cyan) + recipes = client.req_get_recipes_for_item(loot_id) + File.write('disenchanter_recipes.json', recipes.to_json) + puts('Okay, written to disenchanter_recipes.json.') +end + +def debug_save_loot_info(client) + loot_id = ask("Which lootId would you like the info for?\n".light_cyan) + loot_info = client.req_get_loot_info(loot_id) + File.write('disenchanter_lootinfo.json', loot_info.to_json) + puts('Okay, written to disenchanter_lootinfo.json.') +end + +def debug_save_summoner_info(client) + File.write('disenchanter_summoner.json', client.req_get_current_summoner.to_json) + puts('Okay, written to disenchanter_summoner.json.') +end diff --git a/src/modules/handlers/emotes.rb b/src/modules/handlers/emotes.rb new file mode 100644 index 0000000..9890c06 --- /dev/null +++ b/src/modules/handlers/emotes.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require_relative 'generic_loot' + +# Wrapper for emotes +# @param client Client connector +# @note No shards for emotes +def handle_emotes(client) + handle_generic(client, 'Emotes', 'EMOTE') + + client.refresh_loot + # Re-handle icons in case new disenchant candidates came up + handle_generic(client, 'Emotes', 'EMOTE') if handle_esports_emotes(client) +end + +# Rerolls non-disenchantable esports emotes into disenchantable ones +# @return true if re-run is needed +def handle_esports_emotes(client) + esports_emotes = find_esports_emotes(client) + if count_loot_items(esports_emotes).zero? + puts 'Found no Esports Emotes to re-roll.'.yellow + return false + end + + puts "Found #{count_loot_items(esports_emotes)} Esports Emotes." + + unless ans_y.include? user_input_check( + "Re-roll #{count_loot_items(esports_emotes)} already owned Esports Emotes?", + ans_yn, + ans_yn_d, + 'confirm' + ) + return false + end + + client.stat_tracker.add_crafted(count_loot_items(esports_emotes)) + threads = + esports_emotes.map do |g| + Thread.new { client.req_post_recipe('EMOTE_forge', g['lootId'], g['count']) } + end + threads.each(&:join) + + puts 'Done!'.green + client.refresh_loot + + true +end + +def find_esports_emotes(client) + player_loot = client.req_get_player_loot + player_loot.select do |l| + l['type'] == 'EMOTE' \ + && l['disenchantLootName'] == '' \ + && l['redeemableStatus'] == 'ALREADY_OWNED' + end +end diff --git a/src/modules/handlers/eternals.rb b/src/modules/handlers/eternals.rb new file mode 100644 index 0000000..8c8cd12 --- /dev/null +++ b/src/modules/handlers/eternals.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'generic_loot' + +# Wrapper for eternals sets and their shards +# @param client Client connector +def handle_eternals(client) + handle_generic(client, 'Eternals Set Shards', 'STATSTONE_SHARD') + handle_generic(client, 'Eternals Set Permanent', 'STATSTONE') +end diff --git a/src/modules/handlers/exception.rb b/src/modules/handlers/exception.rb new file mode 100644 index 0000000..c43f49f --- /dev/null +++ b/src/modules/handlers/exception.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +def handle_exception(exception, name) + puts "An error occurred while handling #{name}.".light_red + puts 'Please take a screenshot and create an issue at ' \ + 'https://github.com/marvinscham/disenchanter/issues/new'.light_red + puts "If you don't have a GitHub account, send it to dev@marvinscham.de".light_red + puts "Exception Occurred #{exception.class}. Message: #{exception.message}. " \ + "Backtrace: \n #{exception.backtrace.join("\n")}" + puts 'Skipping this step...'.yellow +end diff --git a/src/modules/handlers/generic_loot.rb b/src/modules/handlers/generic_loot.rb new file mode 100644 index 0000000..c62a6dd --- /dev/null +++ b/src/modules/handlers/generic_loot.rb @@ -0,0 +1,139 @@ +# frozen_string_literal: true + +require_relative '../loot_metainfo' +require_relative '../../class/dictionary' + +# Handles generic loot types +# @param client Client connector +# @param name Display name ("Skin Shards") +# @param type Riot loot type name ("SKIN_RENTAL") +def handle_generic(client, name, type) + loot_generic = select_generic_loot(client, type) + if count_loot_items(loot_generic).zero? + puts "Found no #{name} to disenchant.".yellow + return + end + + puts "Found #{count_loot_items(loot_generic)} #{name}.".light_blue + + loot_generic = handle_generic_owned(loot_generic, name) + return if loot_generic == false + + if count_loot_items(loot_generic).zero? + puts "Found no owned #{name} to disenchant.".yellow + return + end + + loot_name_index = loot_generic[0]['itemDesc'] == '' ? 'localizedName' : 'itemDesc' + totals = prepare_generic_totals(loot_generic) + disenchant_info = create_generic_disenchant_info(loot_generic, loot_name_index, name, totals) + + unless ans_y.include? user_input_check( + "Disenchant #{count_loot_items(loot_generic)} #{name} for #{disenchant_info}?", + ans_yn, + ans_yn_d, + 'confirm' + ) + return + end + + execute_generic_disenchant(client, loot_generic, totals) + + puts 'Done!'.green +rescue StandardError => e + handle_exception(e, name) +end + +def select_generic_loot(client, type) + player_loot = client.req_get_player_loot + generic_loot = player_loot.select { |l| l['type'] == type } + # Things like esports icons cannot be disenchanted -> drop + generic_loot.reject { |l| l['disenchantLootName'] == '' } +end + +def handle_generic_owned(loot_generic, name) + contains_unowned_items = false + loot_generic.each do |l| + contains_unowned_items = true if l['redeemableStatus'] != 'ALREADY_OWNED' + end + + if contains_unowned_items + user_option = + user_input_check( + "Keep #{name} you don't own yet?\n".light_cyan + + '[y] '.light_white + "Yes\n".light_cyan + '[n] '.light_white + + "No\n".light_cyan + '[x] '.light_white + + "Exit to main menu\n".light_cyan + 'Option: '.white, + %w[y n x], + '[y|n|x]', + '' + ) + + case user_option + when 'x' + puts 'Action cancelled'.yellow + return false + when 'y' + loot_generic = loot_generic.select { |g| g['redeemableStatus'] == 'ALREADY_OWNED' } + puts "Filtered to #{count_loot_items(loot_generic)} items.".light_blue + when 'n' + # Nothing to do + else + raise StandardError, "This shouldn't be possible yet here we are." + end + end + + loot_generic +end + +def prepare_generic_totals(loot_generic) + totals = { + 'oe' => 0, + 'be' => 0 + } + loot_generic.each do |g| + totals['be'] += g['disenchantValue'] * g['count'] if g['disenchantLootName'] == Dictionary::BLUE_ESSENCE + totals['oe'] += g['disenchantValue'] * g['count'] if g['disenchantLootName'] == Dictionary::ORANGE_ESSENCE + end + + totals +end + +def create_generic_disenchant_info(loot_generic, loot_name_index, name, totals) + loot_generic = loot_generic.sort_by do |l| + [l['redeemableStatus'], l[loot_name_index]] + end + + puts "We'd disenchant #{count_loot_items(loot_generic)} #{name} using the option you chose:".light_blue + loot_generic.each do |l| + create_generic_info_single(l, loot_name_index) + end + + disenchant_info = '' + disenchant_info += "#{totals['oe']} Orange Essence" if totals['oe'].positive? + disenchant_info += ' and ' if totals['be'].positive? && totals['oe'].positive? + disenchant_info += "#{totals['be']} Blue Essence" if totals['be'].positive? + disenchant_info +end + +def create_generic_info_single(loot, loot_name_index) + loot_value = loot['disenchantValue'] * loot['count'] + loot_currency = loot['disenchantLootName'] == Dictionary::BLUE_ESSENCE ? 'BE' : 'OE' + + print pad("#{loot['count']}x ", 5, right: false).light_black + print pad(loot[loot_name_index], 30).light_white + print ' @ '.light_black + print pad("#{loot_value} #{loot_currency}", 8, right: false).light_black + print ' (not owned)'.yellow if loot['redeemableStatus'] != 'ALREADY_OWNED' + puts +end + +def execute_generic_disenchant(client, loot_generic, totals) + client.stat_tracker.add_disenchanted(count_loot_items(loot_generic)) + client.stat_tracker.add_blue_essence(totals['be']) + client.stat_tracker.add_orange_essence(totals['oe']) + threads = loot_generic.map do |g| + Thread.new { client.req_post_recipe(g['disenchantRecipeName'], g['lootId'], g['count']) } + end + threads.each(&:join) +end diff --git a/src/modules/handlers/icons.rb b/src/modules/handlers/icons.rb new file mode 100644 index 0000000..ebda058 --- /dev/null +++ b/src/modules/handlers/icons.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'generic_loot' + +# Wrapper for summoner icons +# @param client Client connector +# @note No shards for icons! +def handle_icons(client) + handle_generic(client, 'Icons', 'SUMMONERICON') +end diff --git a/src/modules/handlers/key_fragments.rb b/src/modules/handlers/key_fragments.rb new file mode 100644 index 0000000..faaaced --- /dev/null +++ b/src/modules/handlers/key_fragments.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Combines key fragments to keys +# @param client Client connector +def handle_key_fragments(client) + player_loot = client.req_get_player_loot + + loot_keys = + player_loot.select { |l| l['lootId'] == 'MATERIAL_key_fragment' } + fragment_count = count_loot_items(loot_keys) + key_count = (count_loot_items(loot_keys) / 3).floor + + if fragment_count < 3 + puts 'Not enough key fragments to craft anything.'.yellow + return + end + + puts "Found #{fragment_count} key fragments.".light_blue + if ans_y.include? user_input_check( + "Craft #{key_count} keys from #{fragment_count} key fragments?", + ans_yn, + ans_yn_d, + 'confirm' + ) + client.stat_tracker.add_crafted(key_count) + client.req_post_recipe( + 'MATERIAL_key_fragment_forge', + 'MATERIAL_key_fragment', + key_count + ) + puts 'Done!'.green + end +rescue StandardError => e + handle_exception(e, 'Key Fragments') +end diff --git a/src/modules/handlers/mastery_tokens.rb b/src/modules/handlers/mastery_tokens.rb new file mode 100644 index 0000000..5e4c761 --- /dev/null +++ b/src/modules/handlers/mastery_tokens.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require_relative '../loot_metainfo' +require_relative '../../class/dictionary' + +# Redeems mastery 6/7 tokens as efficiently as possible: +# Prefers champion shard over champion permanent over blue essence +# @param client Client connector +def handle_mastery_tokens(client) + loot_mastery_tokens = grab_upgradable_tokens(client) + + if loot_mastery_tokens.count.zero? + puts 'Found no upgradable set of Mastery Tokens.'.yellow + return + end + + loot_mastery_tokens = loot_mastery_tokens.sort_by { |l| [l['lootName'], l['itemDesc']] } + puts "We could upgrade the following champions:\n".light_blue + + needed_resources = determine_token_crafting_recources(client, loot_mastery_tokens) + owned_essence = client.req_get_player_loot.select { |l| l['lootId'] == Dictionary::BLUE_ESSENCE }[0]['count'] + + essence_missing = needed_resources['essence'] - owned_essence + if essence_missing.positive? + puts "You're missing #{essence_missing} Blue Essence needed to proceed. Skipping...".yellow + return + end + + execute_token_crafting(client, loot_mastery_tokens, needed_resources) + puts 'Done!'.green +rescue StandardError => e + handle_exception(e, 'token upgrades') +end + +# Reduces player loot to a set of tokens that can be upgraded +# @param client Client connector +def grab_upgradable_tokens(client) + client.req_get_player_loot.select do |l| + (l['lootName'] == Dictionary::MASTERY_6_TOKEN && l['count'] == 2) || + (l['lootName'] == Dictionary::MASTERY_7_TOKEN && l['count'] == 3) + end +end + +# Calculates the cheapest way to upgrade each token set +# @param client Client connector +# @param loot_mastery_tokens Set of upgradable tokens +def determine_token_crafting_recources(client, loot_mastery_tokens) + player_loot = client.req_get_player_loot + needed_resources = { + 'shards' => 0, + 'perms' => 0, + 'essence' => 0 + } + + loot_mastery_tokens.each do |t| + print pad(t['itemDesc'], 15, right: false).light_white + print ' to Mastery Level '.light_black + print t['lootName'][-1].light_white + print ' using '.light_black + + calc_token_crafting_resource(player_loot, t, needed_resources) + puts + end + puts + + needed_resources +end + +def check_token_ref_crafting_material(player_loot, ref_id, type) + type_id = type == 'shard' ? Dictionary::CHAMPION_SHARD : Dictionary::CHAMPION_PERMANENT + + ref_mat = player_loot.select do |l| + l['type'] == type_id && ref_id == l['storeItemId'].to_s + end + + !ref_mat.empty? && ref_mat[0]['count'].positive? +end + +def calc_token_crafting_resource(player_loot, token, needed_resources) + if check_token_ref_crafting_material(player_loot, token['refId'], 'shard') + print 'a champion shard.'.green + needed_resources['shards'] += 1 + token['upgrade_type'] = 'shard' + elsif check_token_ref_crafting_material(player_loot, token['refId'], 'perm') + print 'a champion permanent.'.green + needed_resources['perms'] += 1 + token['upgrade_type'] = 'permanent' + else + recipe_cost = mastery_upgrade_cost(client, (token['lootName'])[-1]) + print "#{recipe_cost} Blue Essence.".yellow + needed_resources['essence'] += recipe_cost + token['upgrade_type'] = 'essence' + end +end + +# Grabs token set blue essence upgrade cost +# @param client Client connector +# @param level Token level (6/7) +def mastery_upgrade_cost(client, level) + recipes = client.req_get_recipes_for_item("#{Dictionary.const_get("MASTERY_#{level}_TOKEN")}-1") + + recipe_cost = recipes.select do |r| + r['recipeName'] == "CHAMPION_TOKEN_#{level}_redeem_withessence" + end + recipe_cost[0]['slots'][1]['quantity'] +end + +# Builds the confirm question string from resources about to be consumed to upgrade tokens +# @param loot_mastery_tokens Upgradable token sets +# @param needed_resources Hash of needed shards, perms and blue essence +def build_token_crafting_confirm_question(loot_mastery_tokens, needed_resources) + question_string = "Upgrade #{loot_mastery_tokens.count} champions using " + question_string += "#{needed_resources['shards']} Shards, " if needed_resources['shards'].positive? + question_string += "#{needed_resources['perms']} Permanents, " if needed_resources['perms'].positive? + question_string += "#{needed_resources['essence']} Blue Essence, " if needed_resources['essence'].positive? + + question_string = question_string.delete_suffix(', ') + "#{question_string}?" +end + +# Will redeem tokens after confirmation +# @param client Client connector +# @param loot_mastery_tokens Upgradable token sets +# @param needed_resources Hash of needed shards, perms and blue essence +def execute_token_crafting(client, loot_mastery_tokens, needed_resources) + unless ans_y.include? user_input_check( + build_token_crafting_confirm_question(loot_mastery_tokens, needed_resources), + ans_yn, + ans_yn_d, + 'confirm' + ) + return + end + + loot_mastery_tokens.each do |t| + target_level = (t['lootName'])[-1] + case t['upgrade_type'] + when 'shard' + client.req_post_recipe( + "CHAMPION_TOKEN_#{target_level}_redeem_withshard", + [t['lootId'], "CHAMPION_RENTAL_#{t['refId']}"], + 1 + ) + when 'permanent' + client.req_post_recipe( + "CHAMPION_TOKEN_#{target_level}_redeem_withpermanent", + [t['lootId'], "CHAMPION_#{t['refId']}"], + 1 + ) + when 'essence' + client.req_post_recipe( + "CHAMPION_TOKEN_#{target_level}_redeem_withessence", + [t['lootId'], 'CURRENCY_champion'], + 1 + ) + else + # Weird, do nothing. + end + + client.stat_tracker.add_redeemed(1) + end +end diff --git a/src/modules/handlers/materials.rb b/src/modules/handlers/materials.rb new file mode 100644 index 0000000..21ae9f1 --- /dev/null +++ b/src/modules/handlers/materials.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require_relative '../../class/menu/materials_menu' + +# Wrapper for materials +# @param client Client connector +def handle_materials(client) + MaterialsMenu.new(client).run_loop +end diff --git a/src/modules/handlers/mythic_essence.rb b/src/modules/handlers/mythic_essence.rb new file mode 100644 index 0000000..c6d526f --- /dev/null +++ b/src/modules/handlers/mythic_essence.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require_relative '../../class/dictionary' +require_relative '../../class/menu/mythic_menu' + +# Handles mythic essence crafting +# @param client Client connector +def handle_mythic_essence(client) + loot_essence = client.req_get_player_loot.select { |l| l['lootId'] == Dictionary::MYTHIC_ESSENCE }[0] + + if loot_essence.nil? || loot_essence['count'].zero? + puts 'Found no Mythic Essence to use.'.yellow + return + end + + puts "Found #{loot_essence['count']} Mythic Essence.".light_blue + + # Determines what to craft + mythic_menu = MythicMenu.new(client) + bail = mythic_menu.run_loop + return if bail + + craft_target_name = mythic_menu.things_todo[mythic_menu.thing_todo] + craft_amount = determine_mythic_craft_amount(craft_target_name, mythic_menu.recipe, loot_essence['count']) + + if craft_amount.zero? + puts 'Not enough Mythic Essence for that.'.yellow + return + end + + execute_mythic_crafting(client, craft_target_name, mythic_menu.recipe, craft_amount) + puts 'Done!'.green +rescue StandardError => e + handle_exception(e, 'Mythic Essence') +end + +# Calculates how the amount of things that can be crafted with user-specified mythic essence +# @param target_name What the thing is called (e.g. Blue Essence, Random Skin Shards, ...) +# @param recipe Recipe to determine cost per unit +# @param essence_owned Owned mythic essence (upper limit for user selection) +def determine_mythic_craft_amount(target_name, recipe, essence_owned) + craft_mythic_amount = user_input_check( + 'Alright, how much Mythic Essence should we use to craft ' \ + "#{target_name}?", + (1..essence_owned.to_i) + .to_a + .append('all') + .append('x') + .map!(&:to_s), + "[1..#{essence_owned}|all|x]" + ) + + if craft_mythic_amount == 'x' + puts 'Mythic crafting canceled.'.yellow + return + end + craft_mythic_amount = essence_owned if craft_mythic_amount == 'all' + craft_mythic_amount = craft_mythic_amount.to_i + + (craft_mythic_amount / recipe['slots'][0]['quantity']).floor +end + +# Confirms and executes crafting +# @param client Client connector +# @param target_name Thing to craft +# @param recipe Recipe to craft with +# @param craft_amount How many things to craft +def execute_mythic_crafting(client, target_name, recipe, craft_amount) + craft_quantity = craft_amount * recipe['outputs'][0]['quantity'] + craft_price = craft_amount * recipe['slots'][0]['quantity'] + + unless ans_y.include? user_input_check( + "Craft #{craft_quantity} #{target_name} from #{craft_price} Mythic Essence?", + ans_yn, + ans_yn_d, + 'confirm' + ) + return + end + + case recipe['outputs'][0]['lootName'] + when Dictionary::BLUE_ESSENCE + client.stat_tracker.add_blue_essence(craft_quantity) + when Dictionary::ORANGE_ESSENCE + client.stat_tracker.add_orange_essence(craft_quantity) + else + # Nothing to track here + end + client.stat_tracker.add_crafted(craft_amount) + + client.req_post_recipe( + recipe['recipeName'], + Dictionary::MYTHIC_ESSENCE, + craft_amount + ) +end diff --git a/src/modules/handlers/skins.rb b/src/modules/handlers/skins.rb new file mode 100644 index 0000000..7a343e8 --- /dev/null +++ b/src/modules/handlers/skins.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'generic_loot' + +# Wrapper for skin shards and permanents +# @param client Client connector +def handle_skins(client) + handle_generic(client, 'Skin Shards', 'SKIN_RENTAL') + handle_generic(client, 'Skin Permanents', 'SKIN') +end diff --git a/src/modules/handlers/tacticians.rb b/src/modules/handlers/tacticians.rb new file mode 100644 index 0000000..011c263 --- /dev/null +++ b/src/modules/handlers/tacticians.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'generic_loot' + +# Wrapper for tacticians +# @param client Client connector +# @note There are no shards for tacticians, only permanents +def handle_tacticians(client) + handle_generic(client, 'Tacticians', 'COMPANION') +end diff --git a/src/modules/handlers/wards.rb b/src/modules/handlers/wards.rb new file mode 100644 index 0000000..b43ee78 --- /dev/null +++ b/src/modules/handlers/wards.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative 'generic_loot' + +# Wrapper for ward skins and their permanents +# @param client Client connector +def handle_ward_skins(client) + handle_generic(client, 'Ward Skin Shards', 'WARDSKIN_RENTAL') + handle_generic(client, 'Ward Skin Permanents', 'WARDSKIN') +end diff --git a/src/modules/loot_metainfo.rb b/src/modules/loot_metainfo.rb new file mode 100644 index 0000000..43f0f08 --- /dev/null +++ b/src/modules/loot_metainfo.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +def count_loot_items(loot_items) + count = 0 + return count if loot_items.nil? || loot_items.empty? + + loot_items.each { |loot| count += loot['count'] } + count +end + +def get_chest_name(client, loot_id) + chest_info = client.req_get_loot_info(loot_id) + return chest_info['localizedName'] unless chest_info['localizedName'].empty? + + catalogue = { + 'CHEST_128' => 'Champion Capsule', + 'CHEST_129' => 'Glorious Champion Capsule', + 'CHEST_210' => 'Honor Level 4 Orb', + 'CHEST_211' => 'Honor Level 5 Orb' + } + + return catalogue[loot_id] if catalogue.key?(loot_id) + + loot_id +end diff --git a/src/modules/open_url.rb b/src/modules/open_url.rb new file mode 100644 index 0000000..9e59a59 --- /dev/null +++ b/src/modules/open_url.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'launchy' + +def open_github + puts 'Opening GitHub repository at https://github.com/marvinscham/disenchanter/ in your browser...'.light_blue + Launchy.open('https://github.com/marvinscham/disenchanter/') +end + +def open_stats + puts 'Opening Global Stats at https://github.com/marvinscham/disenchanter/wiki/Stats in your browser...'.light_blue + Launchy.open('https://github.com/marvinscham/disenchanter/wiki/Stats') +end + +def open_masterychart(client) + server = ask("Which server do you play on (EUW/NA/BR/TR...)?\n".light_cyan) + player = client.req_get_current_summoner + url = "https://masterychart.com/profile/#{server}/#{player['gameName']}-#{player['tagLine']}?ref=disenchanter" + puts "Opening your profile at #{url} in your browser...".light_blue + Launchy.open(url) +end diff --git a/src/modules/stat_submission.rb b/src/modules/stat_submission.rb new file mode 100644 index 0000000..51f312e --- /dev/null +++ b/src/modules/stat_submission.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +def submit_stats(stat_tracker) + uri = URI('https://checksch.de/hook/disenchanter.php') + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + http.request(build_stat_request(uri, stat_tracker)) +rescue StandardError => e + handle_exception(e, 'stat submission') +end + +def build_stat_request(uri, stat_tracker) + req = Net::HTTP::Post.new(uri, 'Content-Type': 'application/json') + req.body = { a: stat_tracker.actions, + d: stat_tracker.disenchanted, + o: stat_tracker.opened, + c: stat_tracker.crafted, + r: stat_tracker.redeemed, + be: stat_tracker.blue_essence, + oe: stat_tracker.orange_essence }.to_json + + req +end + +def handle_stat_submission(stat_tracker) + return if stat_tracker.actions.zero? + + if ans_y.include? user_input_check( + "Would you like to contribute your (anonymous) stats to the global stats?\n".light_cyan + + "#{gather_stats(stat_tracker)}[y|n]: ", + ans_yn, ans_yn_d, + '' + ) + submit_stats(stat_tracker) + puts 'Thank you very much!'.light_green + end +end + +def gather_stats(stat_tracker) + out = "Your stats:\n".light_blue + stats = ['Actions', 'Disenchanted', 'Opened', 'Crafted', 'Redeemed', 'Blue Essence', 'Orange Essence'] + + out + stats.map { |stat| wrap_stat_line(stat, stat_tracker.send(stat.downcase.gsub(' ', '_'))) }.join +end + +def wrap_stat_line(name, value) + strlen = 15 + numlen = 7 + out = pad(name, strlen) + out += pad(value.to_s, numlen, right: false).light_white + "#{out}\n" +end diff --git a/src/modules/update/backwards_compat.rb b/src/modules/update/backwards_compat.rb new file mode 100644 index 0000000..93d0a76 --- /dev/null +++ b/src/modules/update/backwards_compat.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +def backwards_compat + # Doinb 400CS backwards compatibility hack + updater_processes = `tasklist | find /I /C "disenchanter_up.exe"` + + kill_disenchanter if updater_processes.to_i > 2 + + `tasklist|findstr "disenchanter.exe" >nul 2>&1 \ + && echo Backwards compatibility: popping out into separate process... \ + && start cmd.exe @cmd /k "disenchanter_up.exe" \ + && exit` + sleep(1) + kill_disenchanter +end + +def kill_disenchanter + puts 'Killing Disenchanter...' + `taskkill /IM "disenchanter.exe" /F /T >nul 2>&1 && exit` +end diff --git a/src/modules/update/checker.rb b/src/modules/update/checker.rb new file mode 100644 index 0000000..e42c12b --- /dev/null +++ b/src/modules/update/checker.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'openssl' + +def check_update(version_local) + tag_name = grab_remote_tag_name + + version_local = Gem::Version.new(version_local.delete_prefix('v').delete_suffix('-beta')) + version_remote = Gem::Version.new(tag_name.delete_prefix('v').delete_suffix('-beta')) + + if version_remote == version_local + puts "You're up to date!".green + return + end + + if version_local > version_remote + puts 'Welcome to the future!'.light_magenta + puts "Latest remote version: v#{version_remote}".light_blue + return + end + + puts "New version #{tag_name} available!".light_yellow + download_remote_version(tag_name) +rescue StandardError => e + handle_exception(e, 'self update') +end + +def grab_remote_tag_name + uri = URI('https://api.github.com/repos/marvinscham/disenchanter/releases/latest') + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + req = Net::HTTP::Get.new(uri, 'Content-Type': 'application/json') + res = http.request req + JSON.parse(res.body)['tag_name'] +end + +# Downloads the specified version from GitHub +# @param version Disenchanter version to download +def download_remote_version(version) + if ans_y.include? user_input_check( + 'Would you like to download the new version now?', + ans_yn, + ans_yn_d + ) + exe_url = "https://github.com/marvinscham/disenchanter/releases/download/#{version}/disenchanter_up.exe" + `curl #{exe_url} -L -o disenchanter_up.exe` + puts 'Done downloading!'.green + + pid = spawn('start cmd.exe @cmd /k "disenchanter_up.exe"') + Process.detach(pid) + puts 'Exiting...'.light_black + exit + end +end diff --git a/src/modules/update/download.rb b/src/modules/update/download.rb new file mode 100644 index 0000000..f8a99ae --- /dev/null +++ b/src/modules/update/download.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +def download_new_version + ans = fetch_newest_release + + puts "Downloading Disenchanter #{ans['tag_name']}".light_green + `curl https://github.com/marvinscham/disenchanter/releases/download/#{ans['tag_name']}/disenchanter.exe \ + -L -o disenchanter.exe` + + puts '____________________________________________________________'.light_black + puts 'Done downloading!'.green +end + +def fetch_newest_release + uri = URI('https://api.github.com/repos/marvinscham/disenchanter/releases/latest') + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_NONE + req = Net::HTTP::Get.new(uri, 'Content-Type': 'application/json') + res = http.request req + JSON.parse(res.body) +end diff --git a/src/modules/user_input.rb b/src/modules/user_input.rb new file mode 100644 index 0000000..eec30ff --- /dev/null +++ b/src/modules/user_input.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +def ask(question) + print(question) + question = gets + question.chomp +end + +def user_input_check(question, answers, answer_display, color_preset = 'default') + input = '' + + case color_preset + when 'confirm' + question = "CONFIRM: #{question} ".light_magenta + answer_display.to_s.light_white + ': '.light_magenta + when 'dry' + question += " #{answer_display} (DRY): ".light_red + else + question += " #{answer_display}: ".light_white + end + + until answers.include? input + input = ask question + puts 'Invalid answer, options: '.light_red + answer_display.to_s.light_white unless answers.include? input + end + + input +end diff --git a/src/updater.rb b/src/updater.rb new file mode 100644 index 0000000..995b895 --- /dev/null +++ b/src/updater.rb @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'net/https' +require 'base64' +require 'json' +require 'colorize' +require 'open-uri' +require_relative 'modules/update/backwards_compat' +require_relative 'modules/update/download' + +puts 'Grabbing latest version of Disenchanter...'.light_blue + +def run + if File.exist?('./build/.build.lockfile') + puts 'Detected build environment, skipping execution...'.light_yellow + sleep 2 + exit + end + + backwards_compat + download_new_version + + pid = spawn('start cmd.exe @cmd /k "disenchanter.exe"') + Process.detach(pid) + puts 'Exiting...'.light_black +end + +run diff --git a/techstack.md b/techstack.md new file mode 100644 index 0000000..e1b10eb --- /dev/null +++ b/techstack.md @@ -0,0 +1,124 @@ + +
+ +# Tech Stack File +![](https://img.stackshare.io/repo.svg "repo") [marvinscham/disenchanter](https://github.com/marvinscham/disenchanter)![](https://img.stackshare.io/public_badge.svg "public") +

+|12
Tools used|01/05/24
Report generated| +|------|------| +
+ +## Languages (2) + + + + + + +
+ Ruby +
+ Ruby +
+ v3.1.2 +
+ Swift +
+ Swift +
+ +
+ +## DevOps (4) + + + + + + + + + + +
+ Git +
+ Git +
+ +
+ GitHub Actions +
+ GitHub Actions +
+ +
+ RuboCop +
+ RuboCop +
+ v1.50.2 +
+ RubyGems +
+ RubyGems +
+ +
+ +## Other (1) + + + + +
+ CocoaPods +
+ CocoaPods +
+ +
+ + +## Open source packages (5) + +## RubyGems (5) + +|NAME|VERSION|LAST UPDATED|LAST UPDATED BY|LICENSE|VULNERABILITIES| +|:------|:------|:------|:------|:------|:------| +|[colorize](https://rubygems.org/colorize)|v0.8.1|07/25/22|Marvin Scham |GPL-2.0|N/A| +|[json](https://rubygems.org/json)|v2.6.3|07/25/22|Marvin Scham |Ruby|N/A| +|[launchy](https://rubygems.org/launchy)|v2.5.2|07/25/22|Marvin Scham |ISC|N/A| +|[ocra](https://rubygems.org/ocra)|v1.3.11|07/25/22|Marvin Scham |MIT|N/A| +|[rufo](https://rubygems.org/rufo)|v0.16.0|07/25/22|Marvin Scham |MIT|N/A| + +
+
+ +Generated via [Stack File](https://github.com/marketplace/stack-file) diff --git a/techstack.yml b/techstack.yml new file mode 100644 index 0000000..da1573b --- /dev/null +++ b/techstack.yml @@ -0,0 +1,169 @@ +repo_name: marvinscham/disenchanter +report_id: 77abef689a45d775d034168bffbe8c69 +version: 0.1 +repo_type: Public +timestamp: '2024-01-05T08:51:35+00:00' +requested_by: marvinscham +provider: github +branch: main +detected_tools_count: 12 +tools: +- name: Ruby + description: A dynamic, interpreted, open source programming language with a focus + on simplicity and productivity + website_url: https://www.ruby-lang.org + version: 3.1.2 + open_source: true + hosted_saas: false + category: Languages & Frameworks + sub_category: Languages + image_url: https://img.stackshare.io/service/989/ruby.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Repo Metadata + last_updated_by: Marvin Scham + last_updated_on: 2022-07-24 23:09:25.000000000 Z +- name: Swift + description: 'An innovative new programming language for Cocoa and Cocoa Touch. ' + website_url: https://developer.apple.com/swift/ + license: Apache-2.0 + open_source: true + hosted_saas: false + category: Languages & Frameworks + sub_category: Languages + image_url: https://img.stackshare.io/service/1009/tuHsaI2U.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-24 23:09:25.000000000 Z +- name: Git + description: Fast, scalable, distributed revision control system + website_url: http://git-scm.com/ + open_source: true + hosted_saas: false + category: Build, Test, Deploy + sub_category: Version Control System + image_url: https://img.stackshare.io/service/1046/git.png + detection_source_url: https://github.com/marvinscham/disenchanter + detection_source: Repo Metadata +- name: GitHub Actions + description: Automate your workflow from idea to production + website_url: https://github.com/features/actions + open_source: false + hosted_saas: true + category: Build, Test, Deploy + sub_category: Continuous Integration + image_url: https://img.stackshare.io/service/11563/actions.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/.github/workflows/league-patch.yml + detection_source: ".github/workflows/league-patch.yml" + last_updated_by: Marvin Scham + last_updated_on: 2022-07-31 09:16:33.000000000 Z +- name: RuboCop + description: A Ruby static code analyzer, based on the community Ruby style guide + website_url: http://batsov.com/rubocop/ + version: 1.50.2 + license: MIT + open_source: true + hosted_saas: false + category: Build, Test, Deploy + sub_category: Code Review + image_url: https://img.stackshare.io/service/2643/rubocop.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-24 23:09:25.000000000 Z +- name: RubyGems + description: Easily download, install, and use ruby software packages on your system + website_url: https://rubygems.org/ + open_source: false + hosted_saas: false + category: Build, Test, Deploy + sub_category: Package Managers + image_url: https://img.stackshare.io/service/12795/5jL6-BA5_400x400.jpeg + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-25 02:26:36.000000000 Z +- name: CocoaPods + description: A dependency manager for Swift and Objective-C Cocoa projects + website_url: https://cocoapods.org/ + open_source: true + hosted_saas: false + category: Libraries + sub_category: CocoaPods Packages + image_url: https://img.stackshare.io/service/2426/e1cbdef9d4b11484049a033886578e54_400x400.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-24 23:09:25.000000000 Z +- name: colorize + description: Extends String class or add a ColorizedString with methods to set text + color + package_url: https://rubygems.org/colorize + version: 0.8.1 + license: GPL-2.0 + open_source: true + hosted_saas: false + category: Libraries + sub_category: RubyGems Packages + image_url: https://img.stackshare.io/package/18856/default_d343d9a7c573fa5dcbeb4d3c43d2ffe4afa82cc1.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-25 02:26:36.000000000 Z +- name: json + description: This is a JSON implementation as a Ruby extension in C + package_url: https://rubygems.org/json + version: 2.6.3 + license: Ruby + open_source: true + hosted_saas: false + category: Libraries + sub_category: RubyGems Packages + image_url: https://img.stackshare.io/package/18822/default_19184669508c0f71aec9521d5f14d71b77203130.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-25 02:26:36.000000000 Z +- name: launchy + description: Launchy is helper class for launching cross-platform applications in + a fire and forget manner + package_url: https://rubygems.org/launchy + version: 2.5.2 + license: ISC + open_source: true + hosted_saas: false + category: Libraries + sub_category: RubyGems Packages + image_url: https://img.stackshare.io/package/18893/default_421783d7f975d1b076f260bea1d42f0b2621ca39.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-25 02:26:36.000000000 Z +- name: ocra + description: OCRA + package_url: https://rubygems.org/ocra + version: 1.3.11 + license: MIT + open_source: true + hosted_saas: false + category: Libraries + sub_category: RubyGems Packages + image_url: https://img.stackshare.io/package/rubygems/image.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-25 02:26:36.000000000 Z +- name: rufo + description: Fast and unobtrusive Ruby code formatter + package_url: https://rubygems.org/rufo + version: 0.16.0 + license: MIT + open_source: true + hosted_saas: false + category: Libraries + sub_category: RubyGems Packages + image_url: https://img.stackshare.io/package/rubygems/image.png + detection_source_url: https://github.com/marvinscham/disenchanter/blob/main/Gemfile.lock + detection_source: Gemfile + last_updated_by: Marvin Scham + last_updated_on: 2022-07-25 02:26:36.000000000 Z