From 49d501a8b0973553f3ba64007c79703c361009b5 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Tue, 24 Jun 2025 10:38:41 -0500 Subject: [PATCH 1/5] Refactored API to support both backends and to re-enable logging in (on the primary backend) --- Gemfile.lock | 8 +- lib/api.rb | 213 +++++++++++++++++++++++++-------- lib/api/server_list_updater.rb | 2 +- lib/cache.rb | 10 +- lib/pages/community.rb | 2 +- lib/pages/games.rb | 2 +- lib/pages/login.rb | 10 +- lib/states/boot.rb | 6 +- lib/states/interface.rb | 2 +- 9 files changed, 183 insertions(+), 72 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ec93613..821c2a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,14 @@ GEM remote: https://rubygems.org/ specs: - base64 (0.2.0) + base64 (0.3.0) concurrent-ruby (1.3.5) cyberarm_engine (0.24.4) gosu (~> 1.1) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) event_emitter (0.2.6) - excon (1.2.5) + excon (1.2.7) logger ffi (1.17.2-x64-mingw-ucrt) ffi (1.17.2-x86_64-linux-gnu) @@ -21,7 +21,7 @@ GEM libui (0.1.2-x64-mingw) logger (1.7.0) mutex_m (0.3.0) - rake (13.2.1) + rake (13.3.0) rexml (3.4.1) rubyzip (2.4.1) sdl2-bindings (0.2.3) @@ -58,4 +58,4 @@ DEPENDENCIES win32-security BUNDLED WITH - 2.4.3 + 2.6.7 diff --git a/lib/api.rb b/lib/api.rb index 763323b..ca2f931 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -41,12 +41,26 @@ class W3DHub end #! === W3D Hub API === !# + W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # + W3DHUB_API_CONNECTION = Excon.new(W3DHUB_API_ENDPOINT, persistent: true) - ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev" # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # - API_CONNECTION = Excon.new(ENDPOINT, persistent: true) + ALT_W3DHUB_API_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # + ALT_W3DHUB_API_API_CONNECTION = Excon.new(ALT_W3DHUB_API_ENDPOINT, persistent: true) - def self.excon(method, url, headers = DEFAULT_HEADERS, body = nil, api = :api) - logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." } + def self.excon(method, url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + case backend + when :w3dhub + connection = W3DHUB_API_CONNECTION + endpoint = W3DHUB_API_ENDPOINT + when :alt_w3dhub + connection = ALT_W3DHUB_API_API_CONNECTION + endpoint = ALT_W3DHUB_API_ENDPOINT + when :gsh + connection = GSH_CONNECTION + endpoint = SERVER_LIST_ENDPOINT + end + + logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{endpoint}#{url}\"..." } # Inject Authorization header if account data is populated if Store.account @@ -55,9 +69,6 @@ class W3DHub headers["Authorization"] = "Bearer #{Store.account.access_token}" end - connection = api == :api ? API_CONNECTION : GSH_CONNECTION - endpoint = api == :api ? ENDPOINT : SERVER_LIST_ENDPOINT - begin connection.send( method, @@ -86,8 +97,48 @@ class W3DHub end end - def self.post(url, headers = DEFAULT_HEADERS, body = nil, api = :api) - excon(:post, url, headers, body, api) + def self.post(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + excon(:post, url, headers, body, backend) + end + + def self.get(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + excon(:get, url, headers, body, backend) + end + + # Api.get but handles any URL instead of known hosts + def self.fetch(url, headers = DEFAULT_HEADERS, body = nil, backend = nil) + uri = URI(url) + + # Use Api.get for `W3DHUB_API_ENDPOINT` URL's to exploit keep alive and connection reuse (faster responses) + return excon(:get, url, headers, body, backend) if "#{uri.scheme}://#{uri.host}" == W3DHUB_API_ENDPOINT + + logger.debug(LOG_TAG) { "Fetching GET \"#{url}\"..." } + + begin + Excon.get( + url, + headers: headers, + body: body, + nonblock: true, + tcp_nodelay: true, + write_timeout: API_TIMEOUT, + read_timeout: API_TIMEOUT, + connect_timeout: API_TIMEOUT, + idempotent: true, + retry_limit: 3, + retry_interval: 1, + retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout + ) + rescue Excon::Errors::Timeout => e + logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" } + + DummyResponse.new(e) + rescue Excon::Error => e + logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" } + logger.error(LOG_TAG) { e } + + DummyResponse.new(e) + end end # Method: POST @@ -105,24 +156,16 @@ class W3DHub # # On a failed login the service responds with: # {"error":"login-failed"} - def self.refresh_user_login(refresh_token) + def self.refresh_user_login(refresh_token, backend = :w3dhub) body = "data=#{JSON.dump({refreshToken: refresh_token})}" - response = post("#{ENDPOINT}/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body) + response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) if response.status == 200 user_data = JSON.parse(response.body, symbolize_names: true) return false if user_data[:error] - body = "data=#{JSON.dump({ id: user_data[:userid] })}" - user_details = post("#{ENDPOINT}/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body) - - if user_details.status == 200 - user_details_data = JSON.parse(user_details.body, symbolize_names: true) - else - logger.error(LOG_TAG) { "Failed to fetch refresh user details:" } - logger.error(LOG_TAG) { user_details } - end + user_details_data = user_details(user_data[:userid]) || {} Account.new(user_data, user_details_data) else @@ -133,24 +176,16 @@ class W3DHub end # See #user_refresh_token - def self.user_login(username, password) + def self.user_login(username, password, backend = :w3dhub) body = "data=#{JSON.dump({username: username, password: password})}" - response = post("#{ENDPOINT}/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body) + response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) if response.status == 200 user_data = JSON.parse(response.body, symbolize_names: true) return false if user_data[:error] - body = "data=#{JSON.dump({ id: user_data[:userid] })}" - user_details = post("#{ENDPOINT}/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body) - - if user_details.status == 200 - user_details_data = JSON.parse(user_details.body, symbolize_names: true) - else - logger.error(LOG_TAG) { "Failed to fetch user details:" } - logger.error(LOG_TAG) { user_details } - end + user_details_data = user_details(user_data[:userid]) || {} Account.new(user_data, user_details_data) else @@ -160,18 +195,27 @@ class W3DHub end end - # /apis/launcher/1/user-login - # Client sends an Authorization header bearer token which is received from logging in (Required?) + # /apis/w3dhub/1/get-user-details # # Response: avatar-uri (Image download uri), id, username - def self.user_details(id) + def self.user_details(id, backend = :w3dhub) + body = "data=#{JSON.dump({ id: id })}" + user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend) + + if user_details.status == 200 + JSON.parse(user_details.body, symbolize_names: true) + else + logger.error(LOG_TAG) { "Failed to fetch user details:" } + logger.error(LOG_TAG) { user_details } + false + end end # /apis/w3dhub/1/get-service-status # Service response: # {"services":{"authentication":true,"packageDownload":true}} - def self.service_status - response = post("#{ENDPOINT}/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS) + def self.service_status(backend = :w3dhub) + response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend) if response.status == 200 ServiceStatus.new(response.body) @@ -186,8 +230,8 @@ class W3DHub # Client sends an Authorization header bearer token which is received from logging in (Optional) # Launcher sends an empty data request: data={} # Response is a list of applications/games - def self.applications - response = post("#{ENDPOINT}/apis/launcher/1/get-applications") + def self.applications(backend = :w3dhub) + response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend) if response.status == 200 Applications.new(response.body) @@ -198,13 +242,82 @@ class W3DHub end end + # Populate applications list from primary and alternate backends + # (alternate only has latest public builds of _most_ games) + def self._applications + applications_primary = Store.account ? Api.applications(:w3dhub) : false + applications_alternate = Api.applications(:alt_w3dhub) + + # Fail if we fail to fetch applications list from either backend + return false unless applications_primary || applications_alternate + + return applications_alternate unless applications_primary + + # Merge the two app lists together + apps = applications_alternate + if applications_primary + applications_primary.games.each do |game| + # Check if game exists in alternate list + _game = apps.games.find { |g| g.id == game.id } + unless _game + apps.games << game + + # App didn't exist in alternates list + # comparing channels isn't useful + next + end + + # If it does, check that all of its channels also exist in alternate list + # and that the primary versions are the same as the alternates list + game.channels.each do |channel| + _channel = _game.channels.find { |c| c.id == channel.id } + + unless _channel + _game.channels << channel + + # App didn't have channel in alternates list + # comparing channel isn't useful + next + end + + # If channel versions and access levels match then all's well + if channel.current_version == _channel.current_version && + channel.user_level == _channel.user_level + + # All's Well! + next + end + + # If the access levels doen't match then overwrite alternate's channel with primary's channel + if channel.user_level != _channel.user_level + # Replace alternate's channel with primary's channel + _game.channels[_game.channels.index(_channel)] = channel + + # Replaced, continue. + next + end + + # If versions doen't match then pick whichever one is higher + if Gem::Version.new(channel.current_version) > Gem::Version.new(_channel.current_version) + # Replace alternate's channel with primary's channel + _game.channels[_game.channels.index(_channel)] = channel + else + # Do nothing, alternate backend version is greater. + end + end + end + end + + apps + end + # /apis/w3dhub/1/get-news # Client sends an Authorization header bearer token which is received from logging in (Optional) # Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates) # Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one - def self.news(category) + def self.news(category, backend = :w3dhub) body = "data=#{JSON.dump({category: category})}" - response = post("#{ENDPOINT}/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body) + response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend) if response.status == 200 News.new(response.body) @@ -220,9 +333,9 @@ 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(packages) + def self.package_details(packages, backend = :w3dhub) body = URI.encode_www_form("data": JSON.dump({ packages: packages })) - response = post("#{ENDPOINT}/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body) + response = post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend) if response.status == 200 hash = JSON.parse(response.body, symbolize_names: true) @@ -247,9 +360,9 @@ class W3DHub # /apis/w3dhub/1/get-events # # clients requests events: data={"serverPath":"apb"} - def self.events(app_id) + def self.events(app_id, backend = :w3dhub) body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id })) - response = post("#{ENDPOINT}/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body) + response = post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend) if response.status == 200 array = JSON.parse(response.body, symbolize_names: true) @@ -266,10 +379,6 @@ class W3DHub # SERVER_LIST_ENDPOINT = "http://127.0.0.1:9292".freeze GSH_CONNECTION = Excon.new(SERVER_LIST_ENDPOINT, persistent: true) - def self.get(url, headers = DEFAULT_HEADERS, body = nil, api = :api) - excon(:get, url, headers, body, api) - end - # Method: GET # FORMAT: JSON @@ -287,8 +396,8 @@ class W3DHub # id, name, score, kills, deaths # ...players[]: # nick, team (index of teams array), score, kills, deaths - def self.server_list(level = 1) - response = get("#{SERVER_LIST_ENDPOINT}/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, :gsh) + def self.server_list(level = 1, backend = :gsh) + response = get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend) if response.status == 200 data = JSON.parse(response.body, symbolize_names: true) @@ -309,10 +418,10 @@ class W3DHub # id, name, score, kills, deaths # ...players[]: # nick, team (index of teams array), score, kills, deaths - def self.server_details(id, level) + def self.server_details(id, level, backend = :gsh) return false unless id && level - response = get("#{SERVER_LIST_ENDPOINT}/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, :gsh) + response = get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend) if response.status == 200 hash = JSON.parse(response.body, symbolize_names: true) diff --git a/lib/api/server_list_updater.rb b/lib/api/server_list_updater.rb index 902c64e..fb2709f 100644 --- a/lib/api/server_list_updater.rb +++ b/lib/api/server_list_updater.rb @@ -48,7 +48,7 @@ class W3DHub @auto_reconnect = false logger.debug(LOG_TAG) { "Requesting connection token..." } - response = Api.post("#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh) + response = Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh) if response.status != 200 @auto_reconnect = true diff --git a/lib/cache.rb b/lib/cache.rb index 6741946..3c41846 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -9,18 +9,18 @@ class W3DHub end # Fetch a generic uri - def self.fetch(uri:, force_fetch: false, async: true) + def self.fetch(uri:, force_fetch: false, async: true, backend: :w3dhub) path = path(uri) if !force_fetch && File.exist?(path) path elsif async BackgroundWorker.job( - -> { Api.get(uri, W3DHub::Api::DEFAULT_HEADERS) }, + -> { Api.fetch(uri, W3DHub::Api::DEFAULT_HEADERS, nil, backend) }, ->(response) { File.open(path, "wb") { |f| f.write response.body } if response.status == 200 } ) else - response = Api.get(uri, W3DHub::Api::DEFAULT_HEADERS) + response = Api.fetch(uri, W3DHub::Api::DEFAULT_HEADERS, nil, backend) File.open(path, "wb") { |f| f.write response.body } if response.status == 200 end end @@ -69,7 +69,7 @@ class W3DHub body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}" - response = Api.post("#{Api::ENDPOINT}/apis/launcher/1/get-package", headers, body) + response = Api.post("/apis/launcher/1/get-package", headers, body) total_bytes = package.size remaining_bytes = total_bytes - start_from_bytes @@ -89,7 +89,7 @@ class W3DHub # Download a W3D Hub package def self.fetch_package(package, block) - endpoint_download_url = package.download_url || "#{Api::ENDPOINT}/apis/launcher/1/get-package" + endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package" path = package_path(package.category, package.subcategory, package.name, package.version) headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT } headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account && !package.download_url diff --git a/lib/pages/community.rb b/lib/pages/community.rb index 1b95373..926e64a 100644 --- a/lib/pages/community.rb +++ b/lib/pages/community.rb @@ -111,7 +111,7 @@ class W3DHub return unless news news.items[0..15].each do |item| - Cache.fetch(uri: item.image, async: false) + Cache.fetch(uri: item.image, async: false, backend: :w3dhub) end @w3dhub_news = news diff --git a/lib/pages/games.rb b/lib/pages/games.rb index 216a142..3cb44ad 100644 --- a/lib/pages/games.rb +++ b/lib/pages/games.rb @@ -425,7 +425,7 @@ class W3DHub return false unless news news.items[0..15].each do |item| - Cache.fetch(uri: item.image, async: false) + Cache.fetch(uri: item.image, async: false, backend: :w3dhub) end @game_news[game.id] = news diff --git a/lib/pages/login.rb b/lib/pages/login.rb index e35730c..0b9afb1 100644 --- a/lib/pages/login.rb +++ b/lib/pages/login.rb @@ -44,8 +44,10 @@ class W3DHub Store.settings[:account][:data] = account Store.settings.save_settings - Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false) if account - applications = Api.applications if account + if account + Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false, backend: :w3dhub) + applications = Api._applications + end end [account, applications] @@ -79,7 +81,7 @@ class W3DHub if Store.account BackgroundWorker.foreground_job( - -> { Cache.fetch(uri: Store.account.avatar_uri, async: false) }, + -> { Cache.fetch(uri: Store.account.avatar_uri, async: false, backend: :w3dhub) }, ->(result) { populate_account_info page(W3DHub::Pages::Games) @@ -152,7 +154,7 @@ class W3DHub Store.account = nil BackgroundWorker.foreground_job( - -> { Api.applications }, + -> { Api._applications }, lambda do |applications| if applications Store.applications = applications diff --git a/lib/states/boot.rb b/lib/states/boot.rb index 9275bf9..689bf23 100644 --- a/lib/states/boot.rb +++ b/lib/states/boot.rb @@ -127,7 +127,7 @@ class W3DHub Store.settings[:account][:data] = account - Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false) + Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false, backend: :w3dhub) else Store.settings[:account] = {} end @@ -162,7 +162,7 @@ class W3DHub def applications @status_label.value = I18n.t(:"boot.checking_for_updates") - Api.on_thread(:applications) do |applications| + Api.on_thread(:_applications) do |applications| if applications Store.applications = applications Store.settings.save_application_cache(applications.data.to_json) @@ -185,7 +185,7 @@ class W3DHub packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" } end - Api.on_thread(:package_details, packages) do |package_details| + Api.on_thread(:package_details, packages, :alt_w3dhub) do |package_details| package_details ||= nil package_details&.each do |package| diff --git a/lib/states/interface.rb b/lib/states/interface.rb index c789435..4fc4319 100644 --- a/lib/states/interface.rb +++ b/lib/states/interface.rb @@ -138,7 +138,7 @@ class W3DHub if Gosu.milliseconds >= @applications_expire @applications_expire = Gosu.milliseconds + 30_000 - Api.on_thread(:applications) do |applications| + Api.on_thread(:_applications) do |applications| if applications @applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes From ec6dfe8371d5f1090466518dfb3cc3173ef5e88d Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Tue, 24 Jun 2025 10:41:07 -0500 Subject: [PATCH 2/5] Bump version --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index 87859ea..65f2f64 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -1,4 +1,4 @@ class W3DHub DIR_NAME = "W3DHubAlt" - VERSION = "0.6.2" + VERSION = "0.7.0" end \ No newline at end of file From fd728fa945448334a71962663100b7302dac2809 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Tue, 24 Jun 2025 13:47:02 -0500 Subject: [PATCH 3/5] Redid Settings page --- lib/pages/settings.rb | 136 ++++++++++++++++++++---------------------- 1 file changed, 66 insertions(+), 70 deletions(-) diff --git a/lib/pages/settings.rb b/lib/pages/settings.rb index 789cabd..378bfed 100644 --- a/lib/pages/settings.rb +++ b/lib/pages/settings.rb @@ -3,87 +3,57 @@ class W3DHub class Settings < Page def setup body.clear do - stack(width: 1.0, height: 1.0, padding: 16, scroll: true) do + stack(width: 1.0, height: 1.0, padding: 16) do background 0xaa_252525 - stack(width: 1.0, fill: true) do - para "Language" - para "Launcher Language", width: 0.249, margin_left: 32, margin_top: 12 - stack(width: 0.75) do - @language_menu = list_box items: I18n.available_locales.map { |l| expand_language_code(l.to_s) }, choose: expand_language_code(Store.settings[:language]), width: 1.0 - para "Select the UI language you'd like to use in the W3D Hub Launcher." - end - end - - stack(width: 1.0, height: 144) do - para "Folder Paths", margin_top: 8, padding_top: 8, border_thickness_top: 2, border_color_top: 0xee_ffffff, width: 1.0 - flow(width: 1.0, height: 0.5) do - para "App Install Folder", width: 0.249, margin_left: 32, margin_top: 12 - - stack(width: 0.75) do - @app_install_dir_input = edit_line Store.settings[:app_install_dir], width: 1.0 - para "The folder into which new games and apps will be installed by the launcher" - end + stack(width: 1.0, fill: true, max_width: 720, h_align: :center, scroll: true) do + stack(width: 1.0, height: 112) do + tagline "Launcher Language" + @language_menu = list_box items: I18n.available_locales.map { |l| expand_language_code(l.to_s) }, choose: expand_language_code(Store.settings[:language]), width: 1.0, margin_left: 16 + para "Select the UI language you'd like to use in the W3D Hub Launcher.", margin_left: 16 end - flow(width: 1.0, height: 256, margin_top: 16) do - para "Package Cache Folder", width: 0.249, margin_left: 32, margin_top: 12 - stack(width: 0.75, height: 200) do - flow(width: 1.0, height: 1.0) do - @package_cache_dir_input = edit_line Store.settings[:package_cache_dir], fill: true - button "Browse...", width: 128, height: 1.0, tip: "Browse for game executable" do - path = W3DHub.ask_file - end + stack(width: 1.0, height: 200, margin_top: 16) do + tagline "Launcher Directories" + caption "Applications Install Directory", margin_left: 16 + flow(width: 1.0, fill: true, margin_left: 16) do + @app_install_dir_input = edit_line Store.settings[:app_install_dir], fill: true + button "Browse...", width: 128, tip: "Browse for applications install directory" do + path = W3DHub.ask_folder + @app_install_dir_input.value = path unless path.empty? end + end - para "A folder which will be used to cache downloaded packages used to install games and apps" + caption "Package Cache Directory", margin_left: 16, margin_top: 16 + flow(width: 1.0, fill: true, margin_left: 16) do + @package_cache_dir_input = edit_line Store.settings[:package_cache_dir], fill: true + button "Browse...", width: 128, tip: "Browse for package cache directory" do + path = W3DHub.ask_folder + @package_cache_dir_input.value = path unless path.empty? + end + end + end + + if W3DHub.unix? + stack(width: 1.0, height: 224, margin_top: 16) do + tagline "Wine - Windows compatibility layer" + caption "Wine Command", margin_left: 16 + @wine_command_input = edit_line Store.settings[:wine_command], width: 1.0, margin_left: 16 + para "Command to use to for Windows compatiblity layer.", margin_left: 16 + + caption "Wine Prefix", margin_left: 16, margin_top: 16 + flow(width: 1.0, height: 48, margin_left: 16) do + @wine_prefix_toggle = toggle_button checked: Store.settings[:wine_prefix], enabled: false + para "Whether each game gets its own prefix. Uses global/default prefix by default." + end end end end - if true # W3DHub.unix? - stack(width: 1.0, fill: true) do - para "Wine", margin_top: 8, padding_top: 8, border_thickness_top: 2, border_color_top: 0xee_ffffff, width: 1.0 - para "Wine Command", width: 0.249, margin_left: 32, margin_top: 12 - stack(width: 0.75) do - @wine_command_input = edit_line Store.settings[:wine_command], width: 1.0 - para "Command to use to for Windows compatiblity layer" - end - end - - stack(width: 1.0, fill: true, margin_top: 16) do - para "Wine Prefix", width: 0.249, margin_left: 32, margin_top: 12 - stack(width: 0.75) do - @wine_prefix_toggle = toggle_button checked: Store.settings[:wine_prefix] - para "Whether each game gets its own prefix. Uses global/default prefix by default." - end - end - end - - button "Save" do - old_language = Store.settings[:language] - Store.settings[:language] = language_code(@language_menu.value) - - Store.settings[:app_install_dir] = @app_install_dir_input.value - Store.settings[:package_cache_dir] = @package_cache_dir_input.value - - Store.settings[:wine_command] = @wine_command_input.value - Store.settings[:wine_prefix] = @wine_prefix_toggle.value - - Store.settings.save_settings - - begin - I18n.locale = Store.settings[:language] - rescue I18n::InvalidLocale - I18n.locale = :en - end - - if old_language == Store.settings[:language] - page(Pages::Games) - else - # pop back to Boot state which will immediately push a new instance of Interface - pop_state + stack(width: 128, height: 48, h_align: :center, margin_top: 16) do + button "Save", width: 1.0 do + save_settings! end end end @@ -117,6 +87,32 @@ class W3DHub raise "Unknown language error" end end + + def save_settings! + old_language = Store.settings[:language] + Store.settings[:language] = language_code(@language_menu.value) + + Store.settings[:app_install_dir] = @app_install_dir_input.value + Store.settings[:package_cache_dir] = @package_cache_dir_input.value + + Store.settings[:wine_command] = @wine_command_input.value + Store.settings[:wine_prefix] = @wine_prefix_toggle.value + + Store.settings.save_settings + + begin + I18n.locale = Store.settings[:language] + rescue I18n::InvalidLocale + I18n.locale = :en + end + + if old_language == Store.settings[:language] + page(Pages::Games) + else + # pop back to Boot state which will immediately push a new instance of Interface + pop_state + end + end end end end From cc0910e68e0b39788de52bdab320dd08b5dc51d5 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Tue, 24 Jun 2025 13:56:50 -0500 Subject: [PATCH 4/5] Updated cyberarm_engine gem to fix edit_line's with prefilled text not visible --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 821c2a6..297936c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ GEM specs: base64 (0.3.0) concurrent-ruby (1.3.5) - cyberarm_engine (0.24.4) + cyberarm_engine (0.24.5) gosu (~> 1.1) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) From 0bb8ef5f197907653f441b2f67d580897b33f6b6 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Wed, 25 Jun 2025 19:45:23 -0500 Subject: [PATCH 5/5] Initial work launcher (self) updater --- lib/pages/settings.rb | 2 +- lib/states/boot.rb | 27 ++++++++ lib/states/dialogs/launcher_updater_dialog.rb | 62 +++++++++++++++++++ w3d_hub_linux_launcher.rb | 1 + 4 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 lib/states/dialogs/launcher_updater_dialog.rb diff --git a/lib/pages/settings.rb b/lib/pages/settings.rb index 378bfed..3fd0049 100644 --- a/lib/pages/settings.rb +++ b/lib/pages/settings.rb @@ -51,7 +51,7 @@ class W3DHub end end - stack(width: 128, height: 48, h_align: :center, margin_top: 16) do + stack(width: 128, max_height: 256, h_align: :center, margin_top: 16, fill: true) do button "Save", width: 1.0 do save_settings! end diff --git a/lib/states/boot.rb b/lib/states/boot.rb index 689bf23..8f8b9ed 100644 --- a/lib/states/boot.rb +++ b/lib/states/boot.rb @@ -12,6 +12,7 @@ class W3DHub @w3dhub_logo = get_image("#{GAME_ROOT_PATH}/media/icons/app.png") @tasks = { # connectivity_check: { started: false, complete: false }, # HEAD connectivity-check.ubuntu.com or HEAD secure.w3dhub.com? + # launcher_updater: { started: false, complete: false }, server_list: { started: false, complete: false }, refresh_user_token: { started: false, complete: false }, service_status: { started: false, complete: false }, @@ -159,6 +160,32 @@ class W3DHub end end + def launcher_updater + @status_label.value = "Checking for Launcher updates..." # I18n.t(:"boot.checking_for_updates") + + Api.on_thread(:fetch, "https://api.github.com/repos/Inq8/CAmod/releases/latest") do |response| + if response.status == 200 + hash = JSON.parse(response.body, symbolize_names: true) + available_version = hash[:tag_name].downcase.sub("v", "") + + pp Gem::Version.new(available_version) > Gem::Version.new(W3DHub::VERSION) + pp [Gem::Version.new(available_version), Gem::Version.new(W3DHub::VERSION)] + + push_state( + LauncherUpdaterDialog, + release_data: hash, + available_version: available_version, + cancel_callback: -> { @tasks[:launcher_updater][:complete] = true }, + accept_callback: -> { @tasks[:launcher_updater][:complete] = true } + ) + else + # Failed to retrieve release data from github + log "Failed to retrieve release data from Github" + @tasks[:launcher_updater][:complete] = true + end + end + end + def applications @status_label.value = I18n.t(:"boot.checking_for_updates") diff --git a/lib/states/dialogs/launcher_updater_dialog.rb b/lib/states/dialogs/launcher_updater_dialog.rb new file mode 100644 index 0000000..282c303 --- /dev/null +++ b/lib/states/dialogs/launcher_updater_dialog.rb @@ -0,0 +1,62 @@ +class W3DHub + class States + class LauncherUpdaterDialog < Dialog + BUTTON_STYLE = { text_size: 18, padding_top: 3, padding_bottom: 3, padding_left: 3, padding_right: 3, height: 18 } + LIST_ITEM_THEME = Marshal.load(Marshal.dump(THEME)) + BUTTON_STYLE.each do |key, value| + LIST_ITEM_THEME[:Button][key] = value + end + + def setup + window.show_cursor = true + + theme(THEME) + + background 0xaa_525252 + + stack(width: 1.0, max_width: 760, height: 1.0, max_height: 640, v_align: :center, h_align: :center, background: 0xee_222222, border_thickness: 2, border_color: 0xee_222222, padding: 16) do + flow(width: 1.0, height: 36, padding: 8) do + background 0xff_0052c0 + + title @options[:title] || "Launcher Update Available", fill: true, text_align: :center, font: BOLD_FONT + end + + stack(width: 1.0, fill: true, margin_top: 14) do + subtitle "Release Notes - #{@options[:available_version]}" + + # case launcher_release_type + # when :git + # when :tebako + # end + + pp @options[:release_data] + + stack(width: 1.0, fill: true, scroll: true, padding: 8, border_thickness: 1, border_color: 0x44_ffffff) do + # para @options[:release_data][:body], width: 1.0 + # FIXME: Finish this bit + @options[:release_data][:body].lines.each do |line| + line.strip + end + end + end + + flow(width: 1.0, height: 46, margin_top: 16) do + background 0xff_ffffff + + button "Cancel", width: 0.25 do + pop_state + @options[:cancel_callback]&.call + end + + flow(fill: true) + + button "Update", width: 0.25 do + pop_state + @options[:accept_callback]&.call + end + end + end + end + end + end +end diff --git a/w3d_hub_linux_launcher.rb b/w3d_hub_linux_launcher.rb index 9af7df7..622b8ad 100644 --- a/w3d_hub_linux_launcher.rb +++ b/w3d_hub_linux_launcher.rb @@ -121,6 +121,7 @@ require_relative "lib/states/dialogs/confirm_dialog" require_relative "lib/states/dialogs/direct_connect_dialog" require_relative "lib/states/dialogs/game_settings_dialog" require_relative "lib/states/dialogs/import_game_dialog" +require_relative "lib/states/dialogs/launcher_updater_dialog" require_relative "lib/api" require_relative "lib/api/service_status"