diff --git a/Gemfile b/Gemfile index 59f0ee1..a11012f 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem "ffi" gem "websocket-client-simple" gem "thread-local" gem "ircparser" +gem "rubyzip" gem "win32-security", platforms: [:x64_mingw, :mingw] gem "win32-process", platforms: [:x64_mingw, :mingw] diff --git a/Gemfile.lock b/Gemfile.lock index eace382..1f02cfe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GEM digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) event_emitter (0.2.6) - excon (0.93.1) + excon (0.94.0) ffi (1.15.5) ffi (1.15.5-x64-mingw-ucrt) ffi (1.15.5-x64-mingw32) @@ -27,6 +27,7 @@ GEM public_suffix (5.0.0) rake (13.0.6) rexml (3.2.5) + rubyzip (2.3.2) thread-local (1.1.0) websocket (1.2.9) websocket-client-simple (0.6.0) @@ -51,10 +52,11 @@ DEPENDENCIES ircparser launchy rexml + rubyzip thread-local websocket-client-simple win32-process win32-security BUNDLED WITH - 2.3.17 + 2.3.26 diff --git a/lib/api.rb b/lib/api.rb index 4b6ebd1..d9d1b2e 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -45,7 +45,18 @@ class W3DHub end begin - Excon.post(url, headers: headers, body: body, tcp_nodelay: true, write_timeout: API_TIMEOUT, read_timeout: API_TIMEOUT, connection_timeout: API_TIMEOUT) + Excon.post( + url, + headers: headers, + body: body, + tcp_nodelay: true, + write_timeout: API_TIMEOUT, + read_timeout: API_TIMEOUT, + connection_timeout: API_TIMEOUT, + idempotent: true, + retry_limit: 6, + retry_interval: 5 + ) rescue Excon::Errors::Timeout logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" } DummyResponse.new @@ -233,7 +244,7 @@ class W3DHub logger.debug(LOG_TAG) { "Fetching GET \"#{url}\"..." } - Excon.get(url, headers: headers, body: body, persistent: true) + Excon.get(url, headers: headers, body: body, persistent: true, idempotent: true, retry_limit: 6, retry_interval: 5) end # Method: GET diff --git a/lib/application_manager.rb b/lib/application_manager.rb index a712f36..e3cac0f 100644 --- a/lib/application_manager.rb +++ b/lib/application_manager.rb @@ -231,7 +231,24 @@ class W3DHub return false unless server - join_server(app_id, channel, server) + if Store.settings[:server_list_username].to_s.length.zero? + W3DHub.prompt_for_nickname( + accept_callback: proc do |entry| + Store.settings[:server_list_username] = entry + Store.settings.save_settings + + if server.status.password + W3DHub.prompt_for_password( + accept_callback: proc do |password| + join_server(app_id, channel, server) + end + ) + else + join_server(app_id, channel, server) + end + end + ) + end end def favorive(app_id, bool) diff --git a/lib/application_manager/task.rb b/lib/application_manager/task.rb index 1aa85d9..e7ef1e9 100644 --- a/lib/application_manager/task.rb +++ b/lib/application_manager/task.rb @@ -146,10 +146,7 @@ class W3DHub # FIXME: Check that there is enough disk space - # tar present? - bsdtar_present = W3DHub.command("#{W3DHub.tar_command} --help") - fail!("FAIL FAST: `#{W3DHub.tar_command} --help` command failed, #{W3DHub.tar_command} is not installed. Will be unable to unpack packages.") unless bsdtar_present - + # TODO: Is missing wine/proton really a failure condition? # Wine present? if W3DHub.unix? wine_present = W3DHub.command("which #{Store.settings[:wine_command]}") @@ -286,8 +283,8 @@ class W3DHub manifest.files.each do |file| safe_file_name = file.name.gsub("\\", "/") - # Fix borked data -> Data 'cause Windows don't care about capitalization - safe_file_name.sub!("data/", "Data/") # unless File.exist?("#{path}/#{safe_file_name}") + # Fix borked Data -> data 'cause Windows don't care about capitalization + safe_file_name.sub!("Data/", "data/") file_path = "#{path}/#{safe_file_name}" @@ -490,8 +487,6 @@ class W3DHub unpack_package(package, path) end - repair_windows_case_insensitive(package, path) - if status @status.operations[:"#{package.checksum}"].value = package.custom_is_patch ? "Patched" : "Unpacked" @status.operations[:"#{package.checksum}"].progress = 1.0 @@ -635,8 +630,9 @@ class W3DHub logger.info(LOG_TAG) { " #{package.name}:#{package.version}" } package_path = Cache.package_path(package.category, package.subcategory, package.name, package.version) - logger.info(LOG_TAG) { " Running #{W3DHub.tar_command} command: #{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{path}\"" } - return W3DHub.command("#{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{path}\"") + logger.info(LOG_TAG) { " Unpacking package \"#{package_path}\" in \"#{path}\"" } + + return unzip(package_path, path) end def apply_patch(package, path) @@ -647,19 +643,18 @@ class W3DHub Cache.create_directories(temp_path, true) - logger.info(LOG_TAG) { " Running #{W3DHub.tar_command} command: #{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{temp_path}\"" } - W3DHub.command("#{W3DHub.tar_command} -xf \"#{package_path}\" -C \"#{temp_path}\"") + logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" } + unzip(package_path, temp_path) - logger.info(LOG_TAG) { " Loading #{temp_path}/#{manifest_file.name}.patch..." } - patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_path}/#{manifest_file.name}.patch", ignore_crc_mismatches: false) + # Fix borked Data -> data 'cause Windows don't care about capitalization + safe_file_name = "#{manifest_file.name.sub('Data/', 'data/')}" + + logger.info(LOG_TAG) { " Loading #{temp_path}/#{safe_file_name}.patch..." } + patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_path}/#{safe_file_name}.patch", ignore_crc_mismatches: false) patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name == ".w3dhub.patch" || f.name == ".bhppatch" }.data, symbolize_names: true) - repaired_path = "#{path}/#{manifest_file.name}" - # Fix borked data -> Data 'cause Windows don't care about capitalization - repaired_path = "#{path}/#{manifest_file.name.sub('data', 'Data')}" unless File.exist?(repaired_path) && path - - logger.info(LOG_TAG) { " Loading #{repaired_path}..." } - target_mix = W3DHub::Mixer::Reader.new(file_path: repaired_path, ignore_crc_mismatches: false) + logger.info(LOG_TAG) { " Loading #{path}/#{safe_file_name}..." } + target_mix = W3DHub::Mixer::Reader.new(file_path: "#{path}/#{safe_file_name}", ignore_crc_mismatches: false) logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive? patch_info[:removedFiles].each do |file| @@ -681,27 +676,38 @@ class W3DHub end end - logger.info(LOG_TAG) { " Writing updated #{repaired_path}..." } if patch_info[:updatedFiles].size.positive? - W3DHub::Mixer::Writer.new(file_path: repaired_path, package: target_mix.package, memory_buffer: true) + logger.info(LOG_TAG) { " Writing updated #{path}/#{safe_file_name}..." } if patch_info[:updatedFiles].size.positive? + W3DHub::Mixer::Writer.new(file_path: "#{path}/#{safe_file_name}", package: target_mix.package, memory_buffer: true) FileUtils.remove_dir(temp_path) true end - def repair_windows_case_insensitive(package, path) - # Windows is just confused - return true if W3DHub.windows? + def unzip(package_path, path) + stream = Zip::InputStream.new(File.open(package_path)) - # Force data/ to Data/ - return true unless File.exist?("#{path}/data") && File.directory?("#{path}/data") + while (entry = stream.get_next_entry) - logger.info(LOG_TAG) { " Moving #{path}/data/ to #{path}/Data/" } + safe_file_name = entry.name.gsub("\\", "/") + # Fix borked Data -> data 'cause Windows don't care about capitalization + safe_file_name.sub!("Data/", "data/") - FileUtils.mv(Dir.glob("#{path}/data/**"), "#{path}/Data", force: true) - FileUtils.remove_dir("#{path}/data", force: true) + dir_path = "#{path}/#{File.dirname(safe_file_name)}" + unless dir_path.end_with?("/.") || Dir.exist?(dir_path) + FileUtils.mkdir_p(dir_path) + end - true + File.open("#{path}/#{safe_file_name}", "wb") do |f| + i = entry.get_input_stream + + while (chunk = i.read(32_000_000)) # Read up to ~32 MB per chunk + f.write chunk + end + end + end + + return true end end end diff --git a/lib/common.rb b/lib/common.rb index 74f508f..2885475 100644 --- a/lib/common.rb +++ b/lib/common.rb @@ -32,13 +32,48 @@ class W3DHub linux? || mac? end - def self.tar_command - if windows? - "tar" - else - "bsdtar" - end - end + def self.prompt_for_nickname(accept_callback: nil, cancel_callback: nil) + CyberarmEngine::Window.instance.push_state( + W3DHub::States::PromptDialog, + title: I18n.t(:"server_browser.set_nickname"), + message: I18n.t(:"server_browser.set_nickname_message"), + prefill: Store.settings[:server_list_username], + accept_callback: accept_callback, + cancel_callback: cancel_callback, + # See: https://gitlab.com/danpaul88/brenbot/-/blob/master/Source/renlog.pm#L136-175 + valid_callback: proc do |entry| + entry.length > 1 && entry.length < 30 && (entry =~ /(:|!|&|%| )/i).nil? && + (entry =~ /[\001\002\037]/).nil? && (entry =~ /\\/).nil? + end + ) + end + + def self.prompt_for_password(accept_callback: nil, cancel_callback: nil) + CyberarmEngine::Window.instance.push_state( + W3DHub::States::PromptDialog, + title: I18n.t(:"server_browser.enter_password"), + message: I18n.t(:"server_browser.enter_password_message"), + input_type: :password, + accept_callback: accept_callback, + cancel_callback: cancel_callback, + valid_callback: proc { |entry| entry.length.positive? } + ) + end + + def self.join_server(server, password) + if ( + (server.status.password && password.length.positive?) || + !server.status.password) && + Store.settings[:server_list_username].to_s.length.positive? + + Store.application_manager.join_server( + server.game, + server.channel, server, password + ) + else + CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?") + end + end def self.command(command, &block) if windows? diff --git a/lib/pages/server_browser.rb b/lib/pages/server_browser.rb index 6808213..78bdad5 100644 --- a/lib/pages/server_browser.rb +++ b/lib/pages/server_browser.rb @@ -80,7 +80,7 @@ class W3DHub @nickname_label = inscription "#{Store.settings[:server_list_username]}" image "#{GAME_ROOT_PATH}/media/ui_icons/wrench.png", height: 16, hover: { color: 0xaa_ffffff }, tip: I18n.t(:"server_browser.set_nickname") do # Prompt for player name - prompt_for_nickname( + W3DHub.prompt_for_nickname( accept_callback: proc do |entry| @nickname_label.value = entry Store.settings[:server_list_username] = entry @@ -390,32 +390,32 @@ class W3DHub # prompt for password # Launch game if Store.settings[:server_list_username].to_s.length.zero? - prompt_for_nickname( + W3DHub.prompt_for_nickname( accept_callback: proc do |entry| @nickname_label.value = entry Store.settings[:server_list_username] = entry Store.settings.save_settings if server.status.password - prompt_for_password( + W3DHub.prompt_for_password( accept_callback: proc do |password| - join_server(server, password) + W3DHub.join_server(server, password) end ) else - join_server(server, nil) + W3DHub.join_server(server, nil) end end ) else if server.status.password - prompt_for_password( + W3DHub.prompt_for_password( accept_callback: proc do |password| - join_server(server, password) + W3DHub.join_server(server, password) end ) else - join_server(server, nil) + W3DHub.join_server(server, nil) end end end @@ -599,49 +599,6 @@ class W3DHub data end - def prompt_for_nickname(accept_callback: nil, cancel_callback: nil) - push_state( - W3DHub::States::PromptDialog, - title: I18n.t(:"server_browser.set_nickname"), - message: I18n.t(:"server_browser.set_nickname_message"), - prefill: Store.settings[:server_list_username], - accept_callback: accept_callback, - cancel_callback: cancel_callback, - # See: https://gitlab.com/danpaul88/brenbot/-/blob/master/Source/renlog.pm#L136-175 - valid_callback: proc do |entry| - entry.length > 1 && entry.length < 30 && (entry =~ /(:|!|&|%| )/i).nil? && - (entry =~ /[\001\002\037]/).nil? && (entry =~ /\\/).nil? - end - ) - end - - def prompt_for_password(accept_callback: nil, cancel_callback: nil) - push_state( - W3DHub::States::PromptDialog, - title: I18n.t(:"server_browser.enter_password"), - message: I18n.t(:"server_browser.enter_password_message"), - input_type: :password, - accept_callback: accept_callback, - cancel_callback: cancel_callback, - valid_callback: proc { |entry| entry.length.positive? } - ) - end - - def join_server(server, password) - if ( - (server.status.password && password.length.positive?) || - !server.status.password) && - Store.settings[:server_list_username].to_s.length.positive? - - Store.application_manager.join_server( - server.game, - server.channel, server, password - ) - else - window.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?") - end - end - def formatted_score(int) int.to_s.reverse.scan(/.{1,3}/).join(",").reverse end diff --git a/w3d_hub_linux_launcher.rb b/w3d_hub_linux_launcher.rb index 84d537f..ece78fa 100644 --- a/w3d_hub_linux_launcher.rb +++ b/w3d_hub_linux_launcher.rb @@ -7,6 +7,7 @@ require "rexml" require "logger" require "time" require "base64" +require "zip" class W3DHub W3DHUB_DEBUG = ARGV.join.include?("--debug") @@ -55,8 +56,8 @@ end begin require_relative "../cyberarm_engine/lib/cyberarm_engine" rescue LoadError => e - logger.warn(W3D::LOG_TAG) { "Failed to load local cyberarm_engine:" } - logger.warn(W3D::LOG_TAG) { e } + logger.warn(W3DHub::LOG_TAG) { "Failed to load local cyberarm_engine:" } + logger.warn(W3DHub::LOG_TAG) { e } require "cyberarm_engine" end