From 1ab835ba98a562cccc331b4966bce2f1338bfa12 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Mon, 15 Nov 2021 19:22:09 -0600 Subject: [PATCH] Removed idea to have Tasks have a seperate step class; needless abstraction at this point, added manifest parser --- lib/api.rb | 20 +++- lib/api/package.rb | 32 +++++++ lib/application_manager.rb | 5 +- lib/application_manager/manifest.rb | 72 ++++++++++++++ lib/application_manager/task.rb | 104 +++++++++------------ lib/application_manager/tasks/installer.rb | 31 ++++-- lib/cache.rb | 39 +++++++- lib/pages/download_manager.rb | 2 - lib/pages/games.rb | 6 +- lib/pages/login.rb | 10 +- lib/pages/server_browser.rb | 2 +- lib/states/interface.rb | 94 +++++++------------ lib/window.rb | 8 +- w3dhub.rb | 4 + 14 files changed, 280 insertions(+), 149 deletions(-) create mode 100644 lib/api/package.rb create mode 100644 lib/application_manager/manifest.rb diff --git a/lib/api.rb b/lib/api.rb index e08a0f3..8b5cde4 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -143,14 +143,28 @@ class W3DHub # /apis/launcher/1/get-package-details # client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]} - def self.package_details() + def self.package_details(packages) + response = W3DHUB_API_CONNECTION.post( + path: "apis/launcher/1/get-package-details", + headers: DEFAULT_HEADERS.merge({"Content-Type": "application/x-www-form-urlencoded"}), + body: "data=#{JSON.dump({ packages: packages })}" + ) + + if response.status == 200 + hash = JSON.parse(response.body, symbolize_names: true) + packages = hash[:packages].map { |pkg| Package.new(pkg) } + packages.first if packages.size == 1 + else + false + end end # /apis/launcher/1/get-package # client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"} # - # server responds with download bytes - def self.package(category, name, subcategory, version, &block) + # server responds with download bytes, probably supports chunked download and resume + def self.package(category, subcategory, name, version, &block) + Cache.fetch_package(W3DHUB_API_CONNECTION, category, subcategory, name, version, block) end #! === Server List API === !# diff --git a/lib/api/package.rb b/lib/api/package.rb new file mode 100644 index 0000000..8d7221d --- /dev/null +++ b/lib/api/package.rb @@ -0,0 +1,32 @@ +class W3DHub + class Api + class Package + attr_reader :category, :subcategory, :name, :version, :size, :checksum, :checksum_chunk_size, :checksum_chunks + + def initialize(hash) + @data = hash + + @category = @data[:category] + @subcategory = @data[:subcategory] + @name = @data[:name] + @version = @data[:version] + + @size = @data[:size] + @checksum = @data[:checksum] + @checksum_chunk_size = @data[:"checksum-chunk-size"] + @checksum_chunks = @data[:"checksum-chunks"]&.map { |c| Chunk.new(c) } + end + + class Chunk + attr_reader :chunk, :checksum + + def initialize(array) + @data = array + + @chunk = @data[0] + @checksum = @data[1] + end + end + end + end +end diff --git a/lib/application_manager.rb b/lib/application_manager.rb index 999922a..c4e21a3 100644 --- a/lib/application_manager.rb +++ b/lib/application_manager.rb @@ -19,7 +19,10 @@ class W3DHub # unpack packages # install dependencies (e.g. visual C runtime) - @tasks.push(Installer.new(app_id, channel)) + installer = Installer.new(app_id, channel) + + @tasks.push(installer) + installer.start end def import(app_id, channel, path) diff --git a/lib/application_manager/manifest.rb b/lib/application_manager/manifest.rb new file mode 100644 index 0000000..93f1096 --- /dev/null +++ b/lib/application_manager/manifest.rb @@ -0,0 +1,72 @@ +class W3DHub + class ApplicationManager + class Manifest + attr_reader :game, :type, :version, :base_version, :files, :dependencies + + def initialize(category, subcategory, name, version) + manifest = File.read(Cache.package_path(category, subcategory, name, version)) + @document = REXML::Document.new(manifest) + root = @document.root + + @game = root["game"] + @type = root["type"] + @version = root["version"] + @base_version = root["baseVersion"] + + @files = [] + @dependencies = [] + + parse_files + parse_dependencies + end + + def patch? + @type == "Patch" + end + + def full? + @type == "Full" + end + + def parse_files + @document.root.elements.each("//File") do |element| + @files.push(ManifestFile.new(element)) + end + end + + def parse_dependencies + @document.root.elements.each("//Dependency") do |element| + @files.push(Dependency.new(element)) + end + end + + # TODO: Support patches? Are they still a thing> + class ManifestFile + attr_reader :name, :checksum, :package, :removed_since + + def initialize(xml) + @data = xml + + @name = @data["name"] + @checksum = @data["checksum"] + @package = @data["package"] + @removed_since = @data["removedsince"] + end + + def removed? + @removed_since + end + end + + class Dependency + attr_reader :name + + def initialize(xml) + @data = xml + + @name = @data["name"] + end + end + end + end +end diff --git a/lib/application_manager/task.rb b/lib/application_manager/task.rb index 6ca206b..44eb8db 100644 --- a/lib/application_manager/task.rb +++ b/lib/application_manager/task.rb @@ -10,8 +10,6 @@ class W3DHub @release_channel = release_channel @task_state = :not_started # :not_started, :running, :paused, :halted, :complete, :failed - @task_steps = [] - @task_step_index = 0 @application = window.applications.games.find { |g| g.id == app_id } @channel = @application.channels.find { |c| c.name == release_channel } @@ -31,21 +29,14 @@ class W3DHub @task_state = :running Thread.new do - @task_steps.each_with_index do |step, i| - break if @task_state == :halted - - @task_step_index = i - - success = step.start - - failure!(step) unless success - break unless success - end + execute_task @task_state = :complete unless @task_state == :failed end end + def execute_task; end + # Suspend operation, if possible def pause @task_state = :paused if pauseable? @@ -76,69 +67,66 @@ class W3DHub @task_failure_reason || "" end - def failure!(step) + def fail!(reason = "") @task_state = :failed - @task_failure_reason = "Failed to complete: \"#{step.name}\" due to an error: #{step.error}" + @task_failure_reason = "Failed: #{reason}" end def run_on_main_thread(block) window.main_thread_queue << block end - def add_step(name, method, *args) - @task_steps << Step.new(name, method, args) - end - def fetch_manifests - # Do stuff + manifests = [] - package_fetch("games", app_id, "manifest.xml", @channel.version) + if fetch_manifest("games", app_id, "manifest.xml", @channel.current_version) + manifest = load_manifest("games", app_id, "manifest.xml", @channel.current_version) + manifests << manifest + + until(manifest.full?) + fetch_manifest("games", app_id, "manifest.xml", manifest.base_version) + manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version) + manifests << manifest + end + end + + manifests end - def package_fetch(category, subcategory, package, version) + def fetch_manifest(category, subcategory, name, version) + # Check for and integrity of local manifest + if File.exist?(Cache.package_path(category, subcategory, name, version)) + package = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }]) + verified = verify_package(package, category, subcategory, name, version) + + # download manifest if not valid + package_fetch(category, subcategory, name, version) unless verified + true if verified + else + # download manifest if not cached + package_fetch(category, subcategory, name, version) + end end - class Step - attr_reader :name + def package_fetch(category, subcategory, name, version) + Api.package(category, subcategory, name, version) do |chunk, remaining_bytes, total_bytes| + # Store progress somewhere + end + end - def initialize(name, method, args) - @name = name - @method = method - @args = args - - @step_state = :not_started # :not_started, :running, :paused, :halted, :complete, :failed - @success = false + def verify_package(package, category, subcategory, name, version) + digest = Digest::SHA256.new + File.open(Cache.package_path(category, subcategory, name, version)) do |f| + while (chunk = f.read(1_000_000)) + digest.update(chunk) + end end - def start - # do work - # ensure that a boolean value is returned - ensure - @success - end + digest.hexdigest.upcase == package.checksum.upcase + end - def pause - end - - def stop - end - - def status - nil - end - - def progress - 0.0 - end - - def total_work - 1.0 - end - - # data to pass on to next step(s) - def result - nil - end + def load_manifest(category, subcategory, name, version) + Manifest.new(category, subcategory, name, version) end end end diff --git a/lib/application_manager/tasks/installer.rb b/lib/application_manager/tasks/installer.rb index b1cfad6..11774f1 100644 --- a/lib/application_manager/tasks/installer.rb +++ b/lib/application_manager/tasks/installer.rb @@ -1,19 +1,32 @@ class W3DHub class ApplicationManager class Installer < Task - def setup - add_step("Fetching manifests...", :fetch_manifests) - add_step("Building package list...", :build_package_list) + def execute_task + manifests = fetch_manifests + return false if failed? - add_step("Downloading packages...", :fetch_packages) - add_step("Verifying packages...", :verify_packages) - add_step("Unpacking packages...", :unpack_packages) + packages = build_package_list(manifests) + return false if failed? - add_step("Crushing grapes...", :create_wine_prefix) + fetch_packages(packages) + return false if failed? - add_step("Installing dependencies...", :install_dependencies) + verify_packages(packages) + return false if failed? - add_step("Completed.", :mark_application_installed) + unpack_packages(packages) + return false if failed? + + create_wine_prefix + return false if failed? + + install_dependencies(packages) + return false if failed? + + mark_application_installed + return false if failed? + + true end end end diff --git a/lib/cache.rb b/lib/cache.rb index 30da39a..30f3b4a 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -6,6 +6,7 @@ class W3DHub "#{CACHE_PATH}/#{Digest::SHA2.hexdigest(uri)}.#{ext}" end + # Fetch a generic uri def self.fetch(uri) path = path(uri) @@ -26,7 +27,43 @@ class W3DHub end end - def self.fetch_package(*args) + def self.create_directories(path) + target_directory = File.dirname(path) + + FileUtils.mkdir_p(target_directory) unless Dir.exist?(target_directory) + end + + def self.package_path(category, subcategory, name, version) + package_cache_dir = $window.settings[:package_cache_dir] + + "#{package_cache_dir}/#{category}/#{subcategory}/#{version}/#{name}.package" + end + + # Download a W3D Hub package + def self.fetch_package(socket, category, subcategory, name, version, block) + path = package_path(category, subcategory, name, version) + + create_directories(path) + + file = File.open(path, "wb") + + streamer = lambda do |chunk, remaining_bytes, total_bytes| + file.write(chunk) + + block.call(chunk, remaining_bytes, total_bytes) + # puts "Remaining: #{remaining_bytes.to_f / total_bytes}%" + end + + response = socket.post( + path: "apis/launcher/1/get-package", + headers: Api::DEFAULT_HEADERS.merge({"Content-Type": "application/x-www-form-urlencoded"}), + body: "data=#{JSON.dump({ category: category, subcategory: subcategory, name: name, version: version })}", + response_block: streamer + ) + + file.close + + response.status == 200 end end end diff --git a/lib/pages/download_manager.rb b/lib/pages/download_manager.rb index 51d944a..61cd875 100644 --- a/lib/pages/download_manager.rb +++ b/lib/pages/download_manager.rb @@ -61,8 +61,6 @@ class W3DHub # Configure # Disable install # Enable Uninstall - - get end end end diff --git a/lib/pages/games.rb b/lib/pages/games.rb index 38ccb9d..c02c16c 100644 --- a/lib/pages/games.rb +++ b/lib/pages/games.rb @@ -60,10 +60,10 @@ class W3DHub flow(width: 1.0, height: 0.03) do # background 0xff_444411 - puts "Generating..." - inscription "Channel" - list_box items: game.channels.map { |c| c.name }, selected: channel, enabled: game.channels.count > 1, width: 128, padding_left: 4, padding_top: 2, padding_right: 4, padding_bottom: 2, text_size: 16 do |value| + list_box(items: game.channels.map { |c| c.name }, choose: channel.name, enabled: game.channels.count > 1, + margin_top: 0, margin_bottom: 0, + width: 128, padding_left: 1, padding_top: 1, padding_right: 1, padding_bottom: 1, text_size: 14) do |value| populate_game_page(game, game.channels.find{ |c| c.name == value }) end end diff --git a/lib/pages/login.rb b/lib/pages/login.rb index d2f67c7..b5af8d6 100644 --- a/lib/pages/login.rb +++ b/lib/pages/login.rb @@ -40,7 +40,7 @@ class W3DHub account = Api.user_login(@username.value, @password.value) if account - @host.account = account + window.account = account window.settings[:account][:refresh_token] = account.refresh_token window.settings.save_settings @@ -67,7 +67,7 @@ class W3DHub end end - if @host.account + if window.account populate_account_info page(W3DHub::Pages::Games) end @@ -77,17 +77,17 @@ class W3DHub @host.instance_variable_get(:"@account_container").clear do stack(width: 0.7, height: 1.0) do # background 0xff_222222 - tagline "#{@host.account.username}" + tagline "#{window.account.username}" flow(width: 1.0) do link("Logout", text_size: 16) { depopulate_account_info } link "Profile", text_size: 16 do - Launchy.open("https://secure.w3dhub.com/forum/index.php?showuser=#{@host.account.id}") + Launchy.open("https://secure.w3dhub.com/forum/index.php?showuser=#{window.account.id}") end end end - image Cache.path(@host.account.avatar_uri), height: 1.0 + image Cache.path(window.account.avatar_uri), height: 1.0 end end diff --git a/lib/pages/server_browser.rb b/lib/pages/server_browser.rb index a2e537c..84f25e3 100644 --- a/lib/pages/server_browser.rb +++ b/lib/pages/server_browser.rb @@ -163,7 +163,7 @@ class W3DHub end stack(width: 1.0, height: 0.25) do - button "Join Server", enabled: window.application_manager.installed?(server.game) + button "Join Server", enabled: window.application_manager.installed?(server.game, window.applications.games.find { |g| g.id == server.game }.channels.first) end stack(width: 1.0, height: 0.55, margin_top: 16) do diff --git a/lib/states/interface.rb b/lib/states/interface.rb index f1c50d0..84165ee 100644 --- a/lib/states/interface.rb +++ b/lib/states/interface.rb @@ -80,36 +80,45 @@ class W3DHub @app_info_container = flow(width: 1.0, height: 0.65) do # background 0xff_8855ff - stack(width: 0.6749, height: 1.0) do + stack(width: 0.75, height: 1.0) do title "W3D Hub Launcher", height: 0.5 flow(width: 1.0, height: 0.5) do - button( - get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), - tip: "W3D Hub Launcher Settings", - image_height: 1.0, - padding_left: 4, - padding_top: 4, - padding_right: 4, - padding_bottom: 4, - margin_left: 32 - ) do - page(W3DHub::Pages::Settings) + flow(width: 0.18, height: 1.0) do + button( + get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), + tip: "W3D Hub Launcher Settings", + image_height: 1.0, + padding_left: 4, + padding_top: 4, + padding_right: 4, + padding_bottom: 4, + margin_left: 32 + ) do + page(W3DHub::Pages::Settings) + end + + button( + get_image("#{GAME_ROOT_PATH}/media/ui_icons/import.png"), + tip: "Download Manager", + image_height: 1.0, + padding_left: 4, + padding_top: 4, + padding_right: 4, + padding_bottom: 4, + margin_left: 4 + ) do + page(W3DHub::Pages::DownloadManager) + end end - button( - get_image("#{GAME_ROOT_PATH}/media/ui_icons/import.png"), - tip: "Download Manager", - image_height: 1.0, - padding_left: 4, - padding_top: 4, - padding_right: 4, - padding_bottom: 4, - margin_left: 4 - ) do - page(W3DHub::Pages::DownloadManager) - end + stack(width: 0.77, height: 1.0, margin_left: 16) do + flow(width: 1.0, height: 0.65) do + inscription "Downloading Expansive Civilian Warfare...", width: 0.7, text_wrap: :none + inscription "460.2 MB / 254.5 GB", width: 0.3, text_align: :right, text_wrap: :none + end - inscription "Version #{W3DHub::VERSION}", margin_left: 16 + progress fraction: 0.4, height: 2, width: 1.0, fraction_background: 0xff_00acff, border_thickness: 0 + end end end @@ -126,39 +135,6 @@ class W3DHub end end end - - flow(width: 0.075, height: 1.0) do - button( - get_image("#{GAME_ROOT_PATH}/media/ui_icons/minus.png"), - image_width: 16, - padding_left: 4, - padding_top: 4, - padding_right: 4, - padding_bottom: 4, - margin_left: 4 - ) do - # window.minimize - end - - button( - get_image("#{GAME_ROOT_PATH}/media/ui_icons/cross.png"), - image_width: 16, - padding_left: 4, - padding_top: 4, - padding_right: 4, - padding_bottom: 4, - margin_left: 4, - background: 0xff_800000, - hover: { - background: 0xff_a00000 - }, - active: { - background: 0xff_600000 - } - ) do - window.close - end - end end @navigation_container = flow(width: 1.0, height: 0.35) do @@ -191,7 +167,7 @@ class W3DHub end end - if @account + if window.account page(W3DHub::Pages::Login) else page(W3DHub::Pages::Games) diff --git a/lib/window.rb b/lib/window.rb index e824e42..b45f707 100644 --- a/lib/window.rb +++ b/lib/window.rb @@ -19,11 +19,5 @@ class W3DHub super if @application_manager.idle? end - - def button_down(id) - super - - self.borderless = !self.borderless? if id == Gosu::KB_F7 - end end -end +end \ No newline at end of file diff --git a/w3dhub.rb b/w3dhub.rb index e4072b3..bee386a 100644 --- a/w3dhub.rb +++ b/w3dhub.rb @@ -6,7 +6,9 @@ rescue LoadError => e require "cyberarm_engine" end +require "fileutils" require "digest" +require "rexml" require "zlib" require "launchy" @@ -24,6 +26,7 @@ require_relative "lib/window" require_relative "lib/cache" require_relative "lib/settings" require_relative "lib/application_manager" +require_relative "lib/application_manager/manifest" require_relative "lib/application_manager/task" require_relative "lib/application_manager/tasks/installer" require_relative "lib/application_manager/tasks/uninstaller" @@ -38,6 +41,7 @@ require_relative "lib/api/applications" require_relative "lib/api/news" require_relative "lib/api/server_list_server" require_relative "lib/api/account" +require_relative "lib/api/package" require_relative "lib/page" require_relative "lib/pages/games"