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"