18 Commits

Author SHA1 Message Date
a856dd5dd4 Fixed breaking changes from CyberarmEngine styling refactor 2026-03-21 21:14:52 -05:00
38a882179c Refactor automatic server and application refresh into their own methods, added auto refresh for account token 2026-03-20 20:27:11 -05:00
f350a9a937 Use JSON.pretty_generate when saving json (settings, app cache, and asterisk) 2026-03-20 17:46:22 -05:00
2d429cb834 Update hardware survey to be able to collect the same data as on windows, mostly. 2026-03-19 14:37:52 -05:00
d72142a409 Update gems, added logger gem as it is no longer bundled with ruby 2026-03-19 11:57:24 -05:00
9cb41a8693 Change nickname validation to match classic launcher 2026-02-18 08:44:16 -06:00
f024109327 Bump version 2026-02-11 14:31:20 -06:00
287022f2b8 Merge in updates to ApplicationManager from development branch 2026-02-11 14:31:05 -06:00
68df923bea Bump version 2026-01-31 18:25:33 -06:00
ddbec8d72c Fixed patching not preserving encryption flag of target mix 2026-01-31 18:24:47 -06:00
70d4e0c40f WWMix: added support for replacing entries that are duplicates 2026-01-31 18:23:00 -06:00
f30658ffc2 Corrected indention of WebsocketClient, use Async::HTTP::Client instead of Async::HTTP::Internet in order to provide ssl_context with a custom ca_file path in order to support tebako builds again 2026-01-14 19:06:58 -06:00
9e8f4e1c71 Update gems, disable windows packaging gems 2026-01-14 14:54:31 -06:00
b7e2e69af9 Improve missing wine error message, add link to wiki on settings page, fix crash when saving settings due to disabling winetricks options 2026-01-14 14:45:16 -06:00
3dbfd23b10 Limit game events to 1, hide currently unused winetricks bit of settings 2026-01-14 14:04:58 -06:00
d1d667056b Refresh and enable Welcome dialog if no settings.json is present 2026-01-14 12:12:27 -06:00
c881296ac8 Improve wine settings, fixup wineprefix usage 2026-01-14 10:58:55 -06:00
d630e5044e Make clear package cache button functional 2026-01-14 08:12:29 -06:00
20 changed files with 773 additions and 409 deletions

27
Gemfile
View File

@@ -1,15 +1,26 @@
source "https://rubygems.org" source "https://rubygems.org"
# "standard lib" gems
gem "base64" gem "base64"
gem "rexml"
gem "logger"
# networking libs
gem "async-http" gem "async-http"
gem "async-websocket" gem "async-websocket"
# "game" library gem
gem "cyberarm_engine" gem "cyberarm_engine"
gem "sdl2-bindings" gem "sdl2-bindings"
gem "libui", platforms: [:windows]
# misc. libs
gem "digest-crc" gem "digest-crc"
gem "ircparser" gem "ircparser"
gem "rexml"
gem "rubyzip" gem "rubyzip"
# file selection dialogs on windows (SDL3 has these built-in, but we're on SDL2)
gem "libui", platforms: [:windows]
# misc. windows only gems
gem "win32-process", platforms: [:windows] gem "win32-process", platforms: [:windows]
gem "win32-security", platforms: [:windows] gem "win32-security", platforms: [:windows]
@@ -18,9 +29,9 @@ gem "win32-security", platforms: [:windows]
# use `bundle _x.y.z_ COMMAND` to use this one... # use `bundle _x.y.z_ COMMAND` to use this one...
# NOTE: Releasy needs to be installed as a system gem i.e. `rake install` # NOTE: Releasy needs to be installed as a system gem i.e. `rake install`
# NOTE: contents of the `gemhome` folder in the packaged folder need to be moved into the lib/ruby/gems\<RUBY_VERSION> folder # NOTE: contents of the `gemhome` folder in the packaged folder need to be moved into the lib/ruby/gems\<RUBY_VERSION> folder
group :windows_packaging do # group :windows_packaging do
gem "bundler", "~>2.4.3" # gem "bundler", "~>2.4.3"
gem "rake" # gem "rake"
gem "ocran" # gem "ocran"
gem "releasy"#, path: "../releasy" # gem "releasy"#, path: "../releasy"
end # end

View File

@@ -1,13 +1,13 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
async (2.35.1) async (2.38.1)
console (~> 1.29) console (~> 1.29)
fiber-annotation fiber-annotation
io-event (~> 1.11) io-event (~> 1.11)
metrics (~> 0.12) metrics (~> 0.12)
traces (~> 0.18) traces (~> 0.18)
async-http (0.93.0) async-http (0.94.2)
async (>= 2.10.2) async (>= 2.10.2)
async-pool (~> 0.11) async-pool (~> 0.11)
io-endpoint (~> 0.14) io-endpoint (~> 0.14)
@@ -18,7 +18,7 @@ GEM
protocol-http2 (~> 0.22) protocol-http2 (~> 0.22)
protocol-url (~> 0.2) protocol-url (~> 0.2)
traces (~> 0.10) traces (~> 0.10)
async-pool (0.11.1) async-pool (0.11.2)
async (>= 2.0) async (>= 2.0)
async-websocket (0.30.0) async-websocket (0.30.0)
async-http (~> 0.76) async-http (~> 0.76)
@@ -26,12 +26,11 @@ GEM
protocol-rack (~> 0.7) protocol-rack (~> 0.7)
protocol-websocket (~> 0.17) protocol-websocket (~> 0.17)
base64 (0.3.0) base64 (0.3.0)
console (1.34.2) console (1.34.3)
fiber-annotation fiber-annotation
fiber-local (~> 1.1) fiber-local (~> 1.1)
json json
cri (2.15.12) cyberarm_engine (0.25.1)
cyberarm_engine (0.25.0)
gosu (~> 1.1) gosu (~> 1.1)
digest-crc (0.7.0) digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
@@ -44,37 +43,31 @@ GEM
fiber-storage (1.0.1) fiber-storage (1.0.1)
fiddle (1.1.8) fiddle (1.1.8)
gosu (1.4.6) gosu (1.4.6)
io-endpoint (0.16.0) io-endpoint (0.17.2)
io-event (1.14.2) io-event (1.14.4)
io-stream (0.11.1) io-stream (0.11.1)
ircparser (1.0.0) ircparser (1.0.0)
json (2.18.0) json (2.19.2)
libui (0.2.0-x64-mingw-ucrt) libui (0.2.0-x64-mingw-ucrt)
fiddle fiddle
logger (1.7.0)
metrics (0.15.0) metrics (0.15.0)
ocran (1.3.17)
fiddle (~> 1.0)
protocol-hpack (1.5.1) protocol-hpack (1.5.1)
protocol-http (0.58.0) protocol-http (0.60.0)
protocol-http1 (0.36.0) protocol-http1 (0.37.0)
protocol-http (~> 0.58) protocol-http (~> 0.58)
protocol-http2 (0.23.0) protocol-http2 (0.24.0)
protocol-hpack (~> 1.4) protocol-hpack (~> 1.4)
protocol-http (~> 0.47) protocol-http (~> 0.47)
protocol-rack (0.21.0) protocol-rack (0.22.0)
io-stream (>= 0.10) io-stream (>= 0.10)
protocol-http (~> 0.58) protocol-http (~> 0.58)
rack (>= 1.0) rack (>= 1.0)
protocol-url (0.4.0) protocol-url (0.4.0)
protocol-websocket (0.20.2) protocol-websocket (0.20.2)
protocol-http (~> 0.2) protocol-http (~> 0.2)
rack (3.2.4) rack (3.2.5)
rake (13.3.1) rake (13.3.1)
releasy (0.2.4)
bundler (>= 1.2.1)
cri (~> 2.15.0)
ocran (~> 1.3.0)
rake (>= 0.9.2.2)
rexml (3.4.4) rexml (3.4.4)
rubyzip (3.2.2) rubyzip (3.2.2)
sdl2-bindings (0.2.3) sdl2-bindings (0.2.3)
@@ -88,19 +81,17 @@ GEM
PLATFORMS PLATFORMS
x64-mingw-ucrt x64-mingw-ucrt
x86_64-linux
DEPENDENCIES DEPENDENCIES
async-http async-http
async-websocket async-websocket
base64 base64
bundler (~> 2.4.3)
cyberarm_engine cyberarm_engine
digest-crc digest-crc
ircparser ircparser
libui libui
ocran logger
rake
releasy
rexml rexml
rubyzip rubyzip
sdl2-bindings sdl2-bindings
@@ -108,4 +99,4 @@ DEPENDENCIES
win32-security win32-security
BUNDLED WITH BUNDLED WITH
2.4.22 4.0.3

View File

@@ -47,7 +47,9 @@ class W3DHub
W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze #
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_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze #
def self.async_http(method, url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) HTTP_CLIENTS = {}
def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
case backend case backend
when :w3dhub when :w3dhub
endpoint = W3DHUB_API_ENDPOINT endpoint = W3DHUB_API_ENDPOINT
@@ -57,7 +59,15 @@ class W3DHub
endpoint = SERVER_LIST_ENDPOINT endpoint = SERVER_LIST_ENDPOINT
end end
url = "#{endpoint}#{url}" unless url.start_with?("http") # Handle arbitrary urls that may come through
url = nil
if path.start_with?("http")
uri = URI(path)
endpoint = uri.origin
path = uri.request_uri
else
url = "#{endpoint}#{path}"
end
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." } logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
@@ -70,7 +80,7 @@ class W3DHub
Sync do Sync do
begin begin
response = Async::HTTP::Internet.send(method, url, headers, body) response = provision_http_client(endpoint).send(method, path, headers, body)
Response.new(status: response.status, body: response.read) Response.new(status: response.status, body: response.read)
rescue Async::TimeoutError => e rescue Async::TimeoutError => e
@@ -88,17 +98,32 @@ class W3DHub
end end
end end
def self.post(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.provision_http_client(hostname)
async_http(:post, url, headers, body, backend) # Pin http clients to their host Thread so the fiber scheduler doesn't get upset and raise an error
HTTP_CLIENTS[Thread.current] ||= {}
return HTTP_CLIENTS[Thread.current][hostname.downcase] if HTTP_CLIENTS[Thread.current][hostname.downcase]
ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil
ssl_context&.set_params(
ca_file: W3DHub.ca_bundle_path,
verify_mode: OpenSSL::SSL::VERIFY_PEER
)
endpoint = Async::HTTP::Endpoint.parse(hostname, ssl_context: ssl_context)
HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint)
end end
def self.get(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
async_http(:get, url, headers, body, backend) async_http(:post, path, headers, body, backend)
end
def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
async_http(:get, path, headers, body, backend)
end end
# Api.get but handles any URL instead of known hosts # Api.get but handles any URL instead of known hosts
def self.fetch(url, headers = DEFAULT_HEADERS, body = nil, backend = nil) def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil)
async_http(:get, url, headers, body, backend) async_http(:get, path, headers, body, backend)
end end
# Method: POST # Method: POST

View File

@@ -5,6 +5,7 @@ class W3DHub
def initialize def initialize
@tasks = [] # :installer, :importer, :repairer, :uninstaller @tasks = [] # :installer, :importer, :repairer, :uninstaller
@running_applications = {}
end end
def install(app_id, channel) def install(app_id, channel)
@@ -22,9 +23,7 @@ class W3DHub
# unpack packages # unpack packages
# install dependencies (e.g. visual C runtime) # install dependencies (e.g. visual C runtime)
installer = Installer.new(app_id, channel) @tasks.push(Installer.new(app_id, channel))
@tasks.push(installer)
end end
def update(app_id, channel) def update(app_id, channel)
@@ -32,9 +31,7 @@ class W3DHub
return false unless installed?(app_id, channel) return false unless installed?(app_id, channel)
updater = Updater.new(app_id, channel) @tasks.push(Updater.new(app_id, channel))
@tasks.push(updater)
end end
def import(app_id, channel) def import(app_id, channel)
@@ -86,15 +83,15 @@ class W3DHub
# open wwconfig.exe or config.exe for ecw # open wwconfig.exe or config.exe for ecw
if (app_data = installed?(app_id, channel) && W3DHub.unix?) return unless (app_data = installed?(app_id, channel) && W3DHub.unix?)
exe = if Store.settings[:wine_prefix]
"WINEPREFIX=\"#{Store.settings[:wine_prefix]}\" winecfg"
else
"winecfg"
end
Process.spawn("#{exe}") exe = if !Store.settings[:wine_prefix].to_s.empty?
end "WINEPREFIX=\"#{Store.settings[:wine_prefix]}\" winecfg"
else
"winecfg"
end
Process.spawn(exe)
end end
def repair(app_id, channel) def repair(app_id, channel)
@@ -169,11 +166,16 @@ class W3DHub
def wine_command(app_id, channel) def wine_command(app_id, channel)
return "" if W3DHub.windows? return "" if W3DHub.windows?
if Store.settings[:wine_prefix] "\"#{Store.settings[:wine_command]}\" "
"WINEPREFIX=\"#{Store.settings[:wine_prefix]}\" \"#{Store.settings[:wine_command]}\" " end
else
"#{Store.settings[:wine_command]} " def wine_enviroment_variables(app_id, channel)
end vars = {}
return vars if W3DHub.windows?
vars["WINEPREFIX"] = Store.settings[:wine_prefix] unless Store.settings[:wine_prefix].to_s.empty?
vars
end end
def mangohud_command(app_id, channel) def mangohud_command(app_id, channel)
@@ -188,6 +190,13 @@ class W3DHub
end end
end end
def mangohud_enviroment_variables(app_id, channel)
vars = {}
return vars if W3DHub.windows?
vars
end
def dxvk_command(app_id, channel) def dxvk_command(app_id, channel)
return "" if W3DHub.windows? return "" if W3DHub.windows?
@@ -201,6 +210,13 @@ class W3DHub
end end
end end
def dxvk_enviroment_variables(app_id, channel)
vars = {}
return vars if W3DHub.windows?
vars
end
def start_command(path, exe) def start_command(path, exe)
if W3DHub.windows? if W3DHub.windows?
"start /D \"#{path}\" /B #{exe}" "start /D \"#{path}\" /B #{exe}"
@@ -212,16 +228,32 @@ class W3DHub
def run(app_id, channel, *args) def run(app_id, channel, *args)
if (app_data = installed?(app_id, channel)) if (app_data = installed?(app_id, channel))
install_directory = app_data[:install_directory] install_directory = app_data[:install_directory]
exe_path = app_id == "ecw" ? "#{install_directory}/game500.exe" : "#{install_directory}/game.exe" exe_path = app_id == "ecw" ? "#{install_directory}/game500.exe" : app_data[:install_path]
exe_path.gsub!("/", "\\") if W3DHub.windows? exe_path.gsub!("/", "\\") if W3DHub.windows?
exe_path.gsub!("\\", "/") if W3DHub.unix? exe_path.gsub!("\\", "/") if W3DHub.unix?
exe = File.basename(exe_path) exe = File.basename(exe_path)
path = File.dirname(exe_path) path = File.dirname(exe_path)
env = {}
if W3DHub.unix?
env.merge!(
dxvk_enviroment_variables(app_id, channel),
mangohud_enviroment_variables(app_id, channel),
wine_enviroment_variables(app_id, channel)
)
end
attempted = false attempted = false
begin begin
pid = Process.spawn("#{dxvk_command(app_id, channel)}#{mangohud_command(app_id, channel)}#{wine_command(app_id, channel)}#{attempted ? start_command(path, exe) : "\"#{exe_path}\""} -launcher #{args.join(' ')}") pid = Process.spawn(
env,
"#{dxvk_command(app_id, channel)}"\
"#{mangohud_command(app_id, channel)}"\
"#{wine_command(app_id, channel)}"\
"#{attempted ? start_command(path, exe) : "\"#{exe_path}\""} "\
"-launcher #{args.join(' ')}"
)
Process.detach(pid) Process.detach(pid)
rescue Errno::EINVAL => e rescue Errno::EINVAL => e
retryable = !attempted retryable = !attempted
@@ -236,12 +268,14 @@ class W3DHub
end end
def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false) def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false)
if installed?(app_id, channel) && username.to_s.length.positive? return unless installed?(app_id, channel) && username.to_s.length.positive?
run(
app_id, channel, run(
"+connect #{server.address}:#{server.port} +netplayername #{username}#{password ? " +password \"#{password}\"" : ""}#{multi ? " +multi" : ""}" app_id, channel,
) "+connect #{server.address}:#{server.port} "\
end "+netplayername #{username}#{password ? " +password \"#{password}\"" : ""}"\
"#{multi ? " +multi" : ""}"
)
end end
def play_now_server(app_id, channel) def play_now_server(app_id, channel)
@@ -251,9 +285,14 @@ class W3DHub
server_options = Store.server_list.select do |server| server_options = Store.server_list.select do |server|
server.game == app_id && server.game == app_id &&
server.channel == channel && server.channel == channel &&
!server.status.password && !server.status.password &&
server.status.player_count < server.status.max_players server.status.player_count < server.status.max_players
end
# sort by player count HIGH to LOW
# and by ping LOW to HIGH
server_options.sort! do |a, b|
[b.status.player_count, a.ping] <=> [a.status.player_count, b.ping]
end end
# try to find server with lowest ping and matching version # try to find server with lowest ping and matching version
@@ -261,7 +300,7 @@ class W3DHub
# try to find server with lowest ping and undefined version # try to find server with lowest ping and undefined version
found_server ||= server_options.find { |server| server.version == Api::ServerListServer::NO_OR_DEFAULT_VERSION } found_server ||= server_options.find { |server| server.version == Api::ServerListServer::NO_OR_DEFAULT_VERSION }
found_server ? found_server : nil found_server
end end
def play_now(app_id, channel) def play_now(app_id, channel)

View File

@@ -183,7 +183,8 @@ class W3DHub
# Wine present? # Wine present?
if W3DHub.unix? if W3DHub.unix?
wine_present = W3DHub.command("which #{Store.settings[:wine_command]}") wine_present = W3DHub.command("which #{Store.settings[:wine_command]}")
fail!("FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed. Will be unable to create prefixes or launch games.") unless wine_present fail!("FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed.\n\n"\
"Will be unable to launch game.\n\nCheck wine options in launcher's settings.") unless wine_present
end end
end end
@@ -745,6 +746,8 @@ class W3DHub
end end
patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") } patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") }
patch_entry.read patch_entry.read
# "remove" patch meta file from patch before copying patch data
patch_mix.entries.delete(patch_entry)
patch_info = JSON.parse(patch_entry.blob, symbolize_names: true) patch_info = JSON.parse(patch_entry.blob, symbolize_names: true)
@@ -764,20 +767,15 @@ class W3DHub
patch_info[:updatedFiles].each do |file| patch_info[:updatedFiles].each do |file|
logger.debug(LOG_TAG) { " #{file}" } logger.debug(LOG_TAG) { " #{file}" }
patch = patch_mix.entries.find { |e| e.name.casecmp?(file) } patch_mix.entries.each do |entry|
target = target_mix.entries.find { |e| e.name.casecmp?(file) } target_mix.add_entry(entry: entry, replace: true)
if target
target_mix.entries[target_mix.entries.index(target)] = patch
else
target_mix.entries << patch
end end
end end
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive? logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
temp_mix_path = "#{temp_path}/#{File.basename(file_path)}" temp_mix_path = "#{temp_path}/#{File.basename(file_path)}"
temp_mix = W3DHub::WWMix.new(path: temp_mix_path) temp_mix = W3DHub::WWMix.new(path: temp_mix_path, encrypted: target_mix.encrypted?)
target_mix.entries.each { |e| temp_mix.add_entry(entry: e) } target_mix.entries.each { |e| temp_mix.add_entry(entry: e, replace: true) }
unless temp_mix.save unless temp_mix.save
raise temp_mix.error_reason raise temp_mix.error_reason
end end

View File

@@ -76,7 +76,7 @@ class W3DHub
end end
def save_config(config = @config) def save_config(config = @config)
File.write(CONFIG_PATH, config.to_json) File.write(CONFIG_PATH, JSON.pretty_generate(config))
end end
end end
end end

View File

@@ -75,24 +75,24 @@ class W3DHub
result = false result = false
Sync do Sync do
response = nil uri = URI(endpoint_download_url)
Async::HTTP::Internet.send(package.download_url ? :get : :post, endpoint_download_url, headers, body) do |r| response = W3DHub::Api.provision_http_client(uri.origin).send((package.download_url ? :get : :post), uri.request_uri, headers, body)
response = r if response.success?
if r.success? total_bytes = package.size
total_bytes = package.size
r.each do |chunk| response.each do |chunk|
file.write(chunk) file.write(chunk)
block.call(chunk, total_bytes - file.pos, total_bytes) block.call(chunk, total_bytes - file.pos, total_bytes)
end
result = true
end end
result = true
end end
if response.status == 200 || response.status == 206 binding.irb unless response
if response&.status == 200 || response&.status == 206
result = true result = true
else else
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" } logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
@@ -114,11 +114,12 @@ class W3DHub
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" } logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
result = false result = false
ensure
file&.close
response&.close
end end
result result
ensure
file&.close
end end
# Download a W3D Hub package # Download a W3D Hub package

View File

@@ -54,51 +54,49 @@ class W3DHub
end end
def self.prompt_for_nickname(accept_callback: nil, cancel_callback: nil) def self.prompt_for_nickname(accept_callback: nil, cancel_callback: nil)
CyberarmEngine::Window.instance.push_state( CyberarmEngine::Window.instance.push_state(
W3DHub::States::PromptDialog, W3DHub::States::PromptDialog,
title: I18n.t(:"server_browser.set_nickname"), title: I18n.t(:"server_browser.set_nickname"),
message: I18n.t(:"server_browser.set_nickname_message"), message: I18n.t(:"server_browser.set_nickname_message"),
prefill: Store.settings[:server_list_username], prefill: Store.settings[:server_list_username],
accept_callback: accept_callback, accept_callback: accept_callback,
cancel_callback: cancel_callback, cancel_callback: cancel_callback,
# See: https://gitlab.com/danpaul88/brenbot/-/blob/master/Source/renlog.pm#L136-175 valid_callback: proc do |entry|
valid_callback: proc do |entry| entry.length.between?(3, 40) && (entry =~ /^[a-z0-9_\-\[\]]+$/i)
entry.length > 1 && entry.length < 30 && (entry =~ /(:|!|&|%| )/i).nil? &&
(entry =~ /[\001\002\037]/).nil? && (entry =~ /\\/).nil?
end
)
end end
)
end
def self.prompt_for_password(accept_callback: nil, cancel_callback: nil) def self.prompt_for_password(accept_callback: nil, cancel_callback: nil)
CyberarmEngine::Window.instance.push_state( CyberarmEngine::Window.instance.push_state(
W3DHub::States::PromptDialog, W3DHub::States::PromptDialog,
title: I18n.t(:"server_browser.enter_password"), title: I18n.t(:"server_browser.enter_password"),
message: I18n.t(:"server_browser.enter_password_message"), message: I18n.t(:"server_browser.enter_password_message"),
input_type: :password, input_type: :password,
accept_callback: accept_callback, accept_callback: accept_callback,
cancel_callback: cancel_callback, cancel_callback: cancel_callback,
valid_callback: proc { |entry| entry.length.positive? } valid_callback: proc { |entry| entry.length.positive? }
) )
end end
def self.join_server(server:, username: Store.settings[:server_list_username], password: nil, multi: false) def self.join_server(server:, username: Store.settings[:server_list_username], password: nil, multi: false)
if ( if (
(server.status.password && password.length.positive?) || (server.status.password && password.length.positive?) ||
!server.status.password) && !server.status.password) &&
username.to_s.length.positive? username.to_s.length.positive?
Store.application_manager.join_server( Store.application_manager.join_server(
server.game, server.game,
server.channel, server.channel,
server, server,
username, username,
password, password,
multi multi
) )
else else
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?") CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
end end
end end
def self.command(command, &block) def self.command(command, &block)
if windows? if windows?
@@ -123,7 +121,6 @@ class W3DHub
process_info = Process.create(**hash) process_info = Process.create(**hash)
pid = process_info.process_id pid = process_info.process_id
status = -1
until (status = Process.get_exitcode(pid)) until (status = Process.get_exitcode(pid))
if block if block
@@ -140,18 +137,16 @@ class W3DHub
end end
status.zero? status.zero?
else elsif block
if block IO.popen(command, "r") do |io|
IO.popen(command, "r") do |io| io.each_line do |line|
io.each_line do |line| block&.call(line)
block&.call(line)
end
end end
$CHILD_STATUS.success?
else
system(command)
end end
$CHILD_STATUS.success?
else
system(command)
end end
end end
@@ -159,23 +154,26 @@ class W3DHub
File.expand_path("~") File.expand_path("~")
end end
def self.ask_file(title: "Open File", filter: "*game*.exe") def self.ask_file(title: "Open File", filter: "*game*.exe", filters: [])
filters << filter if filters.empty?
if W3DHub.unix? if W3DHub.unix?
# search for command # search for command
cmds = %w{ zenity matedialog qarma kdialog } cmds = %w[zenity matedialog qarma kdialog]
command = cmds.find do |cmd| command = cmds.find do |cmd|
cmd if system("which #{cmd}") cmd if system("which #{cmd}")
end end
path = case File.basename(command) path = case File.basename(command)
when "zenity", "matedialog", "qarma" when "zenity", "matedialog", "qarma"
`#{command} --file-selection --title "#{title}" --file-filter "#{filter}"` options = filters.map { |s| format("--file-filter=\"%s\"", s) }.join(" ")
when "kdialog" `#{command} --file-selection --title \"#{title}\" #{options}`
`#{command} --title "#{title}" --getopenfilename . "#{filter}"` when "kdialog"
else `#{command} --title "#{title}" --getopenfilename . "#{filters.join(" ")}"`
raise "No known command found for system file selection dialog!" else
end raise "No known command found for system file selection dialog!"
end
path.strip path.strip
else else
@@ -189,20 +187,20 @@ class W3DHub
def self.ask_folder(title: "Open Folder") def self.ask_folder(title: "Open Folder")
if W3DHub.unix? if W3DHub.unix?
# search for command # search for command
cmds = %w{ zenity matedialog qarma kdialog } cmds = %w[zenity matedialog qarma kdialog]
command = cmds.find do |cmd| command = cmds.find do |cmd|
cmd if system("which #{cmd}") cmd if system("which #{cmd}")
end end
path = case File.basename(command) path = case File.basename(command)
when "zenity", "matedialog", "qarma" when "zenity", "matedialog", "qarma"
`#{command} --file-selection --directory --title "#{title}"` `#{command} --file-selection --directory --title "#{title}"`
when "kdialog" when "kdialog"
`#{command} --title "#{title}" --getexistingdirectory #{Dir.home}"` `#{command} --title "#{title}" --getexistingdirectory #{Dir.home}"`
else else
raise "No known command found for system file selection dialog!" raise "No known command found for system file selection dialog!"
end end
path.strip path.strip
else else

View File

@@ -2,7 +2,7 @@ class W3DHub
class HardwareSurvey class HardwareSurvey
attr_reader :data attr_reader :data
def initialize def initialize(displays_only: false)
@data = { @data = {
displays: [], displays: [],
system: { system: {
@@ -26,8 +26,6 @@ class W3DHub
} }
} }
# Hardware survey only works on Windows atm
if Gem::win_platform? if Gem::win_platform?
lib_dir = File.dirname($LOADED_FEATURES.find { |file| file.include?("gosu.so") }) lib_dir = File.dirname($LOADED_FEATURES.find { |file| file.include?("gosu.so") })
SDL.load_lib("#{lib_dir}64/SDL2.dll") SDL.load_lib("#{lib_dir}64/SDL2.dll")
@@ -36,11 +34,13 @@ class W3DHub
end end
query_displays query_displays
query_motherboard unless displays_only
query_operating_system query_motherboard
query_cpus query_operating_system
query_ram query_cpus
query_gpus query_ram
query_gpus
end
@data.freeze @data.freeze
end end
@@ -68,37 +68,51 @@ class W3DHub
end end
def query_motherboard def query_motherboard
return unless Gem::win_platform? if Gem::win_platform?
begin
Win32::Registry::HKEY_LOCAL_MACHINE.open("HARDWARE\\DESCRIPTION\\System\\BIOS", Win32::Registry::KEY_READ) do |reg| Win32::Registry::HKEY_LOCAL_MACHINE.open("HARDWARE\\DESCRIPTION\\System\\BIOS", Win32::Registry::KEY_READ) do |reg|
@data[:system][:motherboard][:manufacturer] = safe_reg(reg, "SystemManufacturer") @data[:system][:motherboard][:manufacturer] = safe_reg(reg, "SystemManufacturer")
@data[:system][:motherboard][:model] = safe_reg(reg, "SystemProductName") @data[:system][:motherboard][:model] = safe_reg(reg, "SystemProductName")
@data[:system][:motherboard][:bios_vendor] = safe_reg(reg, "BIOSVendor") @data[:system][:motherboard][:bios_vendor] = safe_reg(reg, "BIOSVendor")
@data[:system][:motherboard][:bios_release_date] = safe_reg(reg, "BIOSReleaseDate") @data[:system][:motherboard][:bios_release_date] = safe_reg(reg, "BIOSReleaseDate")
@data[:system][:motherboard][:bios_version] = safe_reg(reg, "BIOSVersion") @data[:system][:motherboard][:bios_version] = safe_reg(reg, "BIOSVersion")
end
rescue Win32::Registry::Error
@data[:system][:motherboard][:manufacturer] = "Unknown"
@data[:system][:motherboard][:model] = "Unknown"
@data[:system][:motherboard][:bios_vendor] = "Unknown"
@data[:system][:motherboard][:bios_release_date] = "Unknown"
@data[:system][:motherboard][:bios_version] = "Unknown"
end
else # unix
@data[:system][:motherboard][:manufacturer] = safe_file("/sys/devices/virtual/dmi/id/board_vendor")
@data[:system][:motherboard][:model] = safe_file("/sys/devices/virtual/dmi/id/board_name")
@data[:system][:motherboard][:bios_version] = safe_file("/sys/devices/virtual/dmi/id/board_version")
end end
rescue Win32::Registry::Error
@data[:system][:motherboard][:manufacturer] = "Unknown"
@data[:system][:motherboard][:model] = "Unknown"
@data[:system][:motherboard][:bios_vendor] = "Unknown"
@data[:system][:motherboard][:bios_release_date] = "Unknown"
@data[:system][:motherboard][:bios_version] = "Unknown"
end end
def query_operating_system def query_operating_system
return unless Gem::win_platform? if Gem::win_platform?
begin
Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Win32::Registry::KEY_READ) do |reg| Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Win32::Registry::KEY_READ) do |reg|
@data[:system][:operating_system][:name] = safe_reg(reg, "ProductName") @data[:system][:operating_system][:name] = safe_reg(reg, "ProductName")
@data[:system][:operating_system][:build] = safe_reg(reg, "CurrentBuild") @data[:system][:operating_system][:build] = safe_reg(reg, "CurrentBuild")
@data[:system][:operating_system][:version] = safe_reg(reg, "DisplayVersion") @data[:system][:operating_system][:version] = safe_reg(reg, "DisplayVersion")
@data[:system][:operating_system][:edition] = safe_reg(reg, "EditionID") @data[:system][:operating_system][:edition] = safe_reg(reg, "EditionID")
end
rescue Win32::Registry::Error
@data[:system][:operating_system][:name] = "Unknown"
@data[:system][:operating_system][:build] = "Unknown"
@data[:system][:operating_system][:version] = "Unknown"
@data[:system][:operating_system][:edition] = "Unknown"
end
else # unix
release_info = query_release_info
@data[:system][:operating_system][:name] = release_info["pretty_name"] || release_info["name"] || "Unknown"
@data[:system][:operating_system][:build] = release_info["version_codename"] || release_info["build_id"] || "Unknown"
@data[:system][:operating_system][:version] = release_info["version_id"] || release_info["build_id"] || "Unknown"
@data[:system][:operating_system][:edition] = release_info["id"] || release_info["id_like"] || "Unknown"
end end
rescue Win32::Registry::Error
@data[:system][:operating_system][:name] = "Unknown"
@data[:system][:operating_system][:build] = "Unknown"
@data[:system][:operating_system][:version] = "Unknown"
@data[:system][:operating_system][:edition] = "Unknown"
end end
def query_cpus def query_cpus
@@ -122,6 +136,16 @@ class W3DHub
end end
rescue Win32::Registry::Error rescue Win32::Registry::Error
end end
else
cpu_info = query_cpu_info
cpu_info.each do |cpu|
@data[:system][:cpus] << {
manufacturer: cpu["manufacturer"] || "Unknown",
model: cpu["model"] || "Unknown",
mhz: cpu["mhz"] || "Unknown",
family: cpu["family"] || "Unknown"
}
end
end end
instruction_sets = %w[ HasRDTSC HasAltiVec HasMMX Has3DNow HasSSE HasSSE2 HasSSE3 HasSSE41 HasSSE42 HasAVX HasAVX2 HasAVX512F HasARMSIMD HasNEON ] # HasLSX HasLASX # These cause a crash atm instruction_sets = %w[ HasRDTSC HasAltiVec HasMMX Has3DNow HasSSE HasSSE2 HasSSE3 HasSSE41 HasSSE42 HasAVX HasAVX2 HasAVX512F HasARMSIMD HasNEON ] # HasLSX HasLASX # These cause a crash atm
@@ -140,44 +164,57 @@ class W3DHub
end end
def query_gpus def query_gpus
return unless Gem::win_platform? if Gem::win_platform?
begin
Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}", Win32::Registry::KEY_READ) do |reg|
i = 0
Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}", Win32::Registry::KEY_READ) do |reg| reg.each_key do |key, _|
i = 0 next unless key.start_with?("0")
reg.each_key do |key, _| reg.open(key) do |device|
next unless key.start_with?("0")
reg.open(key) do |device|
vram = -1
begin
vram = device["HardwareInformation.qwMemorySize"].to_i
rescue Win32::Registry::Error, TypeError
begin
vram = device["HardwareInformation.MemorySize"].to_i
rescue Win32::Registry::Error, TypeError
vram = -1 vram = -1
begin
vram = device["HardwareInformation.qwMemorySize"].to_i
rescue Win32::Registry::Error, TypeError
begin
vram = device["HardwareInformation.MemorySize"].to_i
rescue Win32::Registry::Error, TypeError
vram = -1
end
end
next if vram.negative?
vram = vram / 1024.0 / 1024.0
@data[:system][:gpus] << {
manufacturer: safe_reg(device, "ProviderName"),
model: safe_reg(device, "DriverDesc"),
vram: vram.round,
driver_date: safe_reg(device, "DriverDate"),
driver_version: safe_reg(device, "DriverVersion")
}
i += 1
end end
end end
next if vram.negative?
vram = vram / 1024.0 / 1024.0
@data[:system][:gpus] << {
manufacturer: safe_reg(device, "ProviderName"),
model: safe_reg(device, "DriverDesc"),
vram: vram.round,
driver_date: safe_reg(device, "DriverDate"),
driver_version: safe_reg(device, "DriverVersion")
}
i += 1
end end
rescue Win32::Registry::Error
end
else # unix
gpu_info = query_glx_info
gpu_info.each do |gpu|
@data[:system][:gpus] << {
manufacturer: gpu["manufacturer"] || "Unknown",
model: gpu["model"] || "Unknown",
vram: gpu["vram"].to_i,
driver_date: gpu["driver_date"] || "Unknown",
driver_version: gpu["driver_version"] || "Unknown"
}
end end
end end
rescue Win32::Registry::Error
end end
def safe_reg(reg, key, default_value = "Unknown") def safe_reg(reg, key, default_value = "Unknown")
@@ -185,5 +222,130 @@ class W3DHub
rescue Win32::Registry::Error rescue Win32::Registry::Error
default_value default_value
end end
def safe_file(path, default_value = "Unknown")
value = File.read(path).to_s.strip
return default_value if value.downcase == "default string"
value
rescue
default_value
end
def query_release_info
hash = {}
File.open("/etc/os-release") do |f|
f.each_line do |line|
line = line.strip
key, value = line.split("=", 2)
value.gsub!('"', "")
hash[key.downcase] = value
end
end
hash
rescue
hash
end
def query_cpu_info
cpus = []
cpu = {}
File.open("/proc/cpuinfo") do |f|
f.each_line do |line|
line = line.strip
if line.empty?
cpu["family"] = format(
"%s Family %s Model %s Stepping %s",
cpu["manufacturer"] || "Unknown",
cpu["_family"] || "Unknown",
cpu["_model"] || "Unknown",
cpu["_stepping"] || "Unknown",
)
cpus << cpu
cpu = {}
next
end
key, value = line.split(":", 2).map(&:strip)
case key.downcase
when "vendor_id"
cpu["manufacturer"] = value
when "model name"
cpu["model"] = value
when "cpu mhz"
cpu["mhz"] = value
when "cpu family"
cpu["_family"] = value
when "model"
cpu["_model"] = value
when "stepping"
cpu["_stepping"] = value
end
end
end
cpus
rescue
cpus
end
def query_glx_info
gpus = []
glxinfo = `glxinfo`
return gpus if glxinfo.empty?
gpu = {}
glxinfo.lines do |line|
line = line.strip
next if line.empty?
key, value = line.split(":", 2).map(&:strip)
mesa_info = false
gpu_memory_info = false
case key.downcase
when "opengl vendor string"
if mesa_info
gpus << gpu
gpu = {}
break
end
when /extended renderer info \(GLX_MESA_query_renderer\)/i
# Joy and happiness
mesa_info = true
when /Memory info \(GL_NVX_gpu_memory_info\)/i
# Happiness and joy
gpu_memory_info = true
when "vendor", "opengl vendor string"
gpu["manufacturer"] = value
when "device", "opengl renderer string"
gpu["model"] = value
when "version"
gpu["driver_version"] = value
when "video memory", "dedicated video memory"
gpu["vram"] = value.gsub(/[\D]+/, "")
when "opengl version string"
gpus << gpu
gpu = {}
break
end
end
gpus
end
end end
end end

View File

@@ -123,7 +123,6 @@ class W3DHub
background_image_path = Cache.package_path(game.category, game.id, "background.png", "") background_image_path = Cache.package_path(game.category, game.id, "background.png", "")
if File.exist?(background_image_path) if File.exist?(background_image_path)
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path) States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path)
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.default[:background_image] = get_image(background_image_path)
end end
# Game Stuff # Game Stuff
@@ -278,7 +277,7 @@ class W3DHub
end end
# Game Events # Game Events
@game_events_container = flow(width: 1.0, height: 128, padding: 8, visible: false) do @game_events_container = stack(width: 1.0, height: 128, padding: 8, scroll: true, visible: false) do
end end
# Game News # Game News
@@ -513,15 +512,15 @@ class W3DHub
@game_events_container.show unless events.empty? @game_events_container.show unless events.empty?
@game_events_container.hide if events.empty? @game_events_container.hide if events.empty?
@game_events_container.clear do return unless (event = events.flatten.first)
events.flatten.each do |event|
stack(fill: true, height: 1.0, margin_left: 8, margin_right: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
background 0x44_000000
title event.title, width: 1.0, text_align: :center @game_events_container.clear do
title event.start_time.strftime("%A"), width: 1.0, text_align: :center stack(width: 1.0, fill: true, margin_left: 8, margin_right: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
caption event.start_time.strftime("%B %e, %Y %l:%M %p"), width: 1.0, text_align: :center background 0x44_000000
end
title event.title, width: 1.0, text_align: :center
title event.start_time.strftime("%A"), width: 1.0, text_align: :center
caption event.start_time.strftime("%B %e, %Y %l:%M %p"), width: 1.0, text_align: :center
end end
end end
end end

View File

@@ -271,9 +271,8 @@ class W3DHub
def stylize_selected_server(server_container) def stylize_selected_server(server_container)
server_container.style.background = @selected_color server_container.style.background = @selected_color
server_container.style.default[:background] = @selected_color server_container.style.hover.background = @selected_color
server_container.style.hover[:background] = @selected_color server_container.style.active.background = @selected_color
server_container.style.active[:background] = @selected_color
end end
def reorder_server_list def reorder_server_list
@@ -284,11 +283,11 @@ class W3DHub
end.reverse!.each_with_index do |child, i| end.reverse!.each_with_index do |child, i|
next if @selected_server_container && child == @selected_server_container next if @selected_server_container && child == @selected_server_container
child.style.hover[:background] = 0xaa_555566 child.style.hover.background = 0xaa_555566
child.style.hover[:active] = 0xaa_555588 child.style.active.background = 0xaa_555588
child.style.default[:background] = 0xaa_333333 if i.even? child.style.background = 0xaa_333333 if i.even?
child.style.default[:background] = 0x00_000000 if i.odd? child.style.background = 0x00_000000 if i.odd?
end end
@server_list_container.recalculate @server_list_container.recalculate

View File

@@ -7,47 +7,74 @@ class W3DHub
background 0xaa_252525 background 0xaa_252525
stack(width: 1.0, fill: true, max_width: 720, h_align: :center, scroll: true) do 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"
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
@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
para "Select the UI language you'd like to use in the W3D Hub Launcher.", margin_left: 16
tagline "Launcher Directories", margin_top: 16
caption "Applications Install Directory", margin_left: 16
flow(width: 1.0, 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 end
caption "Package Cache Directory", margin_left: 16, margin_top: 16
stack(width: 1.0, height: 200, margin_top: 16) do flow(width: 1.0, margin_left: 16) do
tagline "Launcher Directories" @package_cache_dir_input = edit_line Store.settings[:package_cache_dir], fill: true
caption "Applications Install Directory", margin_left: 16 button "Browse...", width: 128, tip: "Browse for package cache directory" do
flow(width: 1.0, fill: true, margin_left: 16) do path = W3DHub.ask_folder
@app_install_dir_input = edit_line Store.settings[:app_install_dir], fill: true @package_cache_dir_input.value = path unless path.empty?
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
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
end end
if W3DHub.unix? if W3DHub.unix?
stack(width: 1.0, height: 224, margin_top: 16) do tagline "Wine - Windows compatibility layer", margin_top: 16
tagline "Wine - Windows compatibility layer" caption "Wine Command", margin_left: 16
caption "Wine Command", margin_left: 16 flow(width: 1.0, margin_left: 16) do
@wine_command_input = edit_line Store.settings[:wine_command], width: 1.0, margin_left: 16 @wine_command_input = edit_line Store.settings[:wine_command], fill: true
para "Command to use to for Windows compatiblity layer.", margin_left: 16 button "Browse...", width: 128, tip: "Browse for wine executable" do
path = W3DHub.ask_file(filters: %w[wine proton])
caption "Wine Prefix", margin_left: 16, margin_top: 16 @wine_command_input.value = path unless path.empty?
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
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, margin_left: 16) do
@wine_prefix_input = edit_line Store.settings[:wine_prefix], fill: true
button "Browse...", width: 128, tip: "Browse for wine prefix directory" do
path = W3DHub.ask_folder
@wine_prefix_input.value = path unless path.empty?
end
end
para "Leave empty to use default global prefix.", margin_left: 16
link "Wiki: Getting Started With Wine", tip: "https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine", margin_top: 16, margin_left: 16, border_color_bottom: 0xff_777777 do
W3DHub.url("https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine")
end
# TODO: support winetricks stuff
# tagline "Winetricks", margin_top: 16
# caption "Winetricks Command", margin_left: 16
# flow(width: 1.0, margin_left: 16) do
# @winetricks_command_input = edit_line Store.settings[:winetricks_command], fill: true, enabled: false
# button "Browse...", width: 128, tip: "Browse for winetricks executable", enabled: false do
# path = W3DHub.ask_file(filters: %w[winetricks protontricks])
# @winetricks_command_input.value = path unless path.empty?
# end
# end
# caption "Fixups", margin_left: 16, margin_top: 16
# button "Install d3dcompiler_47", margin_left: 16, enabled: false
# para "Fixes games instantly crashing at startup due to not being able to compile shaders.", margin_left: 16
# button "Install DXVK", margin_left: 16, margin_top: 16, enabled: false
# para "Use Vulkan-based DirectX translation layers.", margin_left: 16
# para "WARNING: Games will stop working if your hardware does not support Vulkan!", margin_left: 16
end end
end end
@@ -55,12 +82,13 @@ class W3DHub
button "Save", width: 1.0 do button "Save", width: 1.0 do
save_settings! save_settings!
end end
flow(fill: true) flow(fill: true)
end end
button("Clear package cache: #{W3DHub.format_size(Dir.glob("#{Store.settings[:package_cache_dir]}/**/**").map { |f| File.file?(f) ? File.size(f) : 0}.sum)}", **DANGEROUS_BUTTON) do
# TODO. button("Clear package cache: #{W3DHub.format_size(Dir.glob("#{Store.settings[:package_cache_dir]}/**/**").map { |f| File.file?(f) ? File.size(f) : 0}.sum)}", tip: "Purge #{Store.settings[:package_cache_dir]}", **DANGEROUS_BUTTON) do |btn|
logger.info(LOG_TAG) { "Purging cache (#{Store.settings[:package_cache_dir]})..." }
FileUtils.remove_dir(Store.settings[:package_cache_dir], force: true)
btn.value = "Clear package cache: #{W3DHub.format_size(Dir.glob("#{Store.settings[:package_cache_dir]}/**/**").map { |f| File.file?(f) ? File.size(f) : 0}.sum)}"
end end
end end
end end
@@ -104,7 +132,9 @@ class W3DHub
Store.settings[:package_cache_dir] = @package_cache_dir_input.value Store.settings[:package_cache_dir] = @package_cache_dir_input.value
Store.settings[:wine_command] = @wine_command_input.value Store.settings[:wine_command] = @wine_command_input.value
Store.settings[:wine_prefix] = @wine_prefix_toggle.value Store.settings[:wine_prefix] = @wine_prefix_input.value
Store.settings[:winetricks_command] = @winetricks_command_input.value if @winetricks_command_input
Store.settings.save_settings Store.settings.save_settings

View File

@@ -7,7 +7,8 @@ class W3DHub
package_cache_dir: default_package_cache_dir, package_cache_dir: default_package_cache_dir,
parallel_downloads: 4, parallel_downloads: 4,
wine_command: "wine", wine_command: "wine",
create_wine_prefixes: true, wine_prefix: "",
winetricks_command: "winetricks",
allow_diagnostic_reports: false, allow_diagnostic_reports: false,
server_list_username: "", server_list_username: "",
server_list_filters: {}, server_list_filters: {},
@@ -66,10 +67,18 @@ class W3DHub
def load_settings def load_settings
@settings = JSON.parse(File.read(SETTINGS_FILE_PATH), symbolize_names: true) @settings = JSON.parse(File.read(SETTINGS_FILE_PATH), symbolize_names: true)
# FIXUPS
# FOR: v0.9.0
@settings.delete(:create_wine_prefixes)
@settings[:wine_prefix] ||= ""
@settings[:winetricks_command] ||= "winetricks"
@settings
end end
def save_settings def save_settings
File.write(SETTINGS_FILE_PATH, @settings.to_json) File.write(SETTINGS_FILE_PATH, JSON.pretty_generate(@settings))
end end
def save_application_cache(json) def save_application_cache(json)

View File

@@ -230,7 +230,7 @@ class W3DHub
Api.on_thread(:_applications) do |applications| Api.on_thread(:_applications) do |applications|
if applications if applications
Store.applications = applications Store.applications = applications
Store.settings.save_application_cache(applications.data.to_json) Store.settings.save_application_cache(JSON.pretty_generate(applications.data))
@tasks[:applications][:complete] = true @tasks[:applications][:complete] = true
else else
# FIXME: Failed to retreive! # FIXME: Failed to retreive!

View File

@@ -23,6 +23,7 @@ class W3DHub
@service_status = @options[:service_status] @service_status = @options[:service_status]
@applications = @options[:applications] @applications = @options[:applications]
@account_expire = Gosu.milliseconds
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes @applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # 5 minutes @server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # 5 minutes
@@ -59,25 +60,21 @@ class W3DHub
link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE @interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
page(W3DHub::Pages::ServerBrowser) page(W3DHub::Pages::ServerBrowser)
end end
link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE @interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
page(W3DHub::Pages::Community) page(W3DHub::Pages::Community)
end end
link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE @interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
page(W3DHub::Pages::DownloadManager) page(W3DHub::Pages::DownloadManager)
end end
link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE @interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
page(W3DHub::Pages::Settings) page(W3DHub::Pages::Settings)
end end
end end
@@ -132,6 +129,13 @@ class W3DHub
end end
hide_application_taskbar hide_application_taskbar
every(3_000) do
# NOTE: each method called, internally checks whether it should act.
refresh_account_token
refresh_applications
refresh_server_list
end
end end
def draw def draw
@@ -146,36 +150,6 @@ class W3DHub
@page&.update @page&.update
update_interface_task_status(@interface_task_update_pending) if @interface_task_update_pending update_interface_task_status(@interface_task_update_pending) if @interface_task_update_pending
if Gosu.milliseconds >= @applications_expire
@applications_expire = Gosu.milliseconds + 30_000
Api.on_thread(:_applications) do |applications|
if applications
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
Store.applications = applications
# TODO: Signal Games and ServerBrowser that applications have been updated
end
end
end
if Gosu.milliseconds >= @server_list_expire
@server_list_expire = Gosu.milliseconds + 30_000
Api.on_thread(:server_list, 2) do |list|
if list
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # five minutes
Store.server_list_last_fetch = Gosu.milliseconds
Api::ServerListUpdater.instance.refresh_server_list(list)
BackgroundWorker.foreground_job(-> {}, ->(_) { States::Interface.instance&.update_server_browser(nil, :refresh_all) })
end
end
end
end end
def button_down(id) def button_down(id)
@@ -274,6 +248,63 @@ class W3DHub
end end
end end
end end
def refresh_account_token
return if Gosu.milliseconds < @account_expire
return unless account = Store.account
@account_expire = Gosu.milliseconds + 30_000
if (account.access_token_expiry - Time.now) / 60 <= 60 * 3 # Refresh if token expires within 3 hours
logger.info(LOG_TAG) { "Refreshing user login..." }
Api.on_thread(:refresh_user_login, account.refresh_token) do |refreshed_account|
if refreshed_account
Store.account = refreshed_account
Store.settings[:account][:data] = refreshed_account
else
Store.settings[:account] = {}
end
Store.settings.save_settings
end
end
end
def refresh_applications
return if Gosu.milliseconds < @applications_expire
@applications_expire = Gosu.milliseconds + 30_000
Api.on_thread(:_applications) do |applications|
if applications
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
Store.applications = applications
# TODO: Signal Games and ServerBrowser that applications have been updated
end
end
end
def refresh_server_list
return if Gosu.milliseconds < @server_list_expire
@server_list_expire = Gosu.milliseconds + 30_000
Api.on_thread(:server_list, 2) do |list|
if list
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # five minutes
Store.server_list_last_fetch = Gosu.milliseconds
Api::ServerListUpdater.instance.refresh_server_list(list)
BackgroundWorker.foreground_job(-> {}, ->(_) { States::Interface.instance&.update_server_browser(nil, :refresh_all) })
end
end
end
end end
end end
end end

View File

@@ -10,7 +10,7 @@ class W3DHub
flow(width: 1.0, height: 1.0, background_image: "#{GAME_ROOT_PATH}/media/banners/background.png", background_image_color: 0xff_525252, background_image_mode: :fill) do flow(width: 1.0, height: 1.0, background_image: "#{GAME_ROOT_PATH}/media/banners/background.png", background_image_color: 0xff_525252, background_image_mode: :fill) do
flow(fill: true) flow(fill: true)
@card_container = stack(width: 1.0, max_width: MAX_PAGE_WIDTH, height: 1.0, max_height: 720, margin: 128, padding: 16) do @card_container = stack(width: 1.0, max_width: MAX_PAGE_WIDTH, height: 1.0, max_height: 720, margin: 64, v_align: :center, h_align: :center, padding: 16) do
background 0xaa_353535 background 0xaa_353535
end end
@@ -24,9 +24,12 @@ class W3DHub
def card_welcome def card_welcome
stack(width: 1.0, fill: true) do stack(width: 1.0, fill: true) do
banner "Welcome", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_000000 banner "Welcome", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
title "Welcome to the #{I18n.t(:app_name_simple)}" title "Welcome to the #{I18n.t(:app_name_simple)}"
caption "The #{I18n.t(:app_name_simple)} is a one-stop shop for your W3D gaming needs, providing game downloads, automatic updating, an integrated server browser, and centralized management of in-game options.", width: 1.0, margin_left: 32 caption "The #{I18n.t(:app_name_simple)} is a one-stop shop for your W3D gaming needs, providing game downloads, "\
"automatic updating, an integrated server browser, and centralized management of in-game options.", width: 1.0, margin_left: 32
image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 256
end end
flow(width: 1.0, height: 46) do flow(width: 1.0, height: 46) do
@@ -44,14 +47,25 @@ class W3DHub
def card_getting_started def card_getting_started
stack(width: 1.0, fill: true) do stack(width: 1.0, fill: true) do
banner "Getting Started", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_000000 banner "Getting Started", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
title "Import C&C Renegade" title "Import Command & Conquer: Renegade"
caption "You can import your installed copy of Renegade if it wasn't automatically imported from the Games tab. If you need to procure a copy of Renegade, EA's Origin Store has the Command & Conquer The Ultimate Collection available. We cannot provide Renegade for installation.", width: 1.0, margin_left: 32 caption "You can import your installed copy of Renegade if it wasn't automatically imported from the Games tab.\n"\
"If you need to procure a copy of Renegade, Both Steam and the EA App have the Command & Conquer The Ultimate Collection available for purchase. "\
"We cannot provide Renegade for installation.", width: 1.0, margin_left: 32
stack(width: 1.0, height: 2, background: 0x88_ffffff) stack(width: 1.0, height: 2, background: 0xff_0074e0, margin_top: 16, margin_bottom: 16)
title "Install one of our standalone games" title "Install one of our standalone games"
caption "Browse our selection of games from the left panel of the Games tab.\n• Interim Apex - Renegade but with hundreds of vehicles and characters.\n• Red Alert: A Path Beyond - DESCRIPTION\n• Tiberian Sun: Reborn - DESCRIPTION\n\nAnd more... Check out the left panel on the Games tab.", width: 1.0, margin_left: 32 stack(width: 1.0, fill: true, margin_left: 32) do
tagline "Interim Apex"
caption "An expanded boots on the ground conflict set after the advent of Tiberian Dawn and the inter-war period between Tiberian Dawn and Tiberian Sun.", margin_left: 16
tagline "Red Alert 2: Apocalypse Rising"
caption "A multiplayer first-and-third-person shooter set in the vibrant universe of Command & Conquer: Red Alert 2. ", margin_left: 16
tagline "Tiberian Sun: Reborn"
caption "A standalone first-person shooter set in the Tiberian Sun universe.", margin_left: 16
para ""
caption "And more games! See them all on the Games tab."
end
end end
flow(width: 1.0, height: 46) do flow(width: 1.0, height: 46) do
@@ -66,25 +80,22 @@ class W3DHub
end end
button "Next >" do button "Next >" do
@card_container.clear { card_communitiy } @card_container.clear { W3DHub.unix? ? card_wine : card_community }
end end
end end
end end
def card_communitiy def card_wine
stack(width: 1.0, fill: true) do stack(width: 1.0, fill: true) do
banner "W3D Hub Community", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_000000 banner "Wine - Windows compatibility layer", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
title "Forums" stack(width: 1.0, fill: true, margin_left: 32) do
caption "Join our forum community", margin_left: 32 title "Got Wine?"
caption "The launcher requires a windows compatibility tool like wine in order to run the games.", margin_left: 32
title "Facebook" caption "Install wine and winetricks through your distribution's package manager or use a wine manager like Bottles.", margin_left: 32
caption "Like us on Facebook", margin_left: 32 link "See most up to date instructions on the wiki.", tip: "https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine", margin_top: 16, margin_left: 32, border_color_bottom: 0xff_777777 do
W3DHub.url("https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine")
title "Discord" end
caption "Join our Discord community server", margin_left: 32 end
title "YouTube"
caption "Subscribe to our YouTube channel", margin_left: 32
end end
flow(width: 1.0, height: 46) do flow(width: 1.0, height: 46) do
@@ -92,6 +103,52 @@ class W3DHub
button "< Back" do button "< Back" do
@card_container.clear { card_getting_started } @card_container.clear { card_getting_started }
end end
link "Skip", border_color_bottom: 0xff_777777, margin_left: 16 do
pop_state
end
end
button "Next >" do
@card_container.clear { card_community }
end
end
end
def card_community
stack(width: 1.0, fill: true) do
banner "W3D Hub Community", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
title "W3D Hub"
link "Visit website", tip: "https://w3dhub.com", margin_left: 32, border_color_bottom: 0xff_777777 do
W3DHub.url("https://w3dhub.com")
end
title "Forum"
link "Join our forum community", tip: "https://w3dhub.com/forum", margin_left: 32, border_color_bottom: 0xff_777777 do
W3DHub.url("https://w3dhub.com/forum")
end
title "Facebook"
link "Like us on Facebook", tip: "https://www.facebook.com/w3dhub/", margin_left: 32, border_color_bottom: 0xff_777777 do
W3DHub.url("https://www.facebook.com/w3dhub/")
end
title "Discord"
link "Join our Discord community server", tip: "https://discord.gg/jMmmRa2", margin_left: 32, border_color_bottom: 0xff_777777 do
W3DHub.url("https://discord.gg/jMmmRa2")
end
title "YouTube"
link "Subscribe to our YouTube channel", tip: "https://www.youtube.com/@w3dhub-official", margin_left: 32, border_color_bottom: 0xff_777777 do
W3DHub.url("https://www.youtube.com/@w3dhub-official")
end
end
flow(width: 1.0, height: 46) do
flow(fill: true, height: 1.0) do
button "< Back" do
@card_container.clear { W3DHub.unix? ? card_wine : card_getting_started }
end
end end
button "Done" do button "Done" do

View File

@@ -1,4 +1,4 @@
class W3DHub class W3DHub
DIR_NAME = "W3DHubAlt".freeze DIR_NAME = "W3DHubAlt".freeze
VERSION = "0.9.0".freeze VERSION = "0.9.2".freeze
end end

View File

@@ -1,68 +1,75 @@
class W3DHub class W3DHub
class WebSocketClient class WebSocketClient
def initialize def initialize
@errored = nil @errored = nil
@connection = nil @connection = nil
@events = { @events = {
open: nil, open: nil,
message: nil, message: nil,
close: nil, close: nil,
error: nil error: nil
} }
end end
def connect(endpoint, headers: nil, &block) def connect(endpoint, headers: nil, &block)
yield(self) yield(self)
Sync do |task| Sync do |task|
endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names) ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil
ssl_context&.alpn_protocols = Async::HTTP::Protocol::HTTP11.names
ssl_context&.set_params(
ca_file: W3DHub.ca_bundle_path,
verify_mode: OpenSSL::SSL::VERIFY_PEER
)
Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection| endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names, ssl_context: ssl_context)
@connection = connection
@events[:open]&.call Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection|
@connection = connection
while message = connection.read @events[:open]&.call
@events[:message].call(message)
while message = connection.read
@events[:message].call(message)
end
# FIXME: Don't rescue for all ta errors?
rescue => error
@errored = true
@events[:error]&.call(error)
ensure
@events[:close]&.call unless @errored
@connection = nil
@errored = false
end end
# FIXME: Don't rescue for all ta errors?
rescue => error
@errored = true
@events[:error]&.call(error)
ensure
@events[:close]&.call unless @errored
@connection = nil
@errored = false
end end
end
self self
end end
def on(event, &block) def on(event, &block)
raise "Event must be a symbol" unless event.is_a?(Symbol) raise "Event must be a symbol" unless event.is_a?(Symbol)
raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event) raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event)
raise "No block given for #{event.inspect}" unless block_given? raise "No block given for #{event.inspect}" unless block_given?
@events[event] = block @events[event] = block
end end
def send(data, type: :text) def send(data, type: :text)
@connection&.write(data) @connection&.write(data)
@connection&.flush @connection&.flush
end end
def close def close
@connection&.close @connection&.close
end end
def open? def open?
!closed? !closed?
end end
def closed? def closed?
@connection&.closed? @connection&.closed?
end end
end end
end end

View File

@@ -17,8 +17,8 @@ class W3DHub
end end
# push_state(W3DHub::States::DemoInputDelay) # push_state(W3DHub::States::DemoInputDelay)
# push_state(W3DHub::States::Welcome)
push_state(W3DHub::States::Boot) push_state(W3DHub::States::Boot)
push_state(W3DHub::States::Welcome) unless File.exist?(SETTINGS_FILE_PATH)
# push_state(W3DHub::States::DirectConnectDialog) # push_state(W3DHub::States::DirectConnectDialog)
# push_state(W3DHub::Asterisk::States::IRCProfileForm) # push_state(W3DHub::Asterisk::States::IRCProfileForm)
end end

View File

@@ -228,27 +228,34 @@ class W3DHub
@encrypted @encrypted
end end
def add_file(path:) def add_file(path:, replace: false)
return false unless File.exist?(path) return false unless File.exist?(path)
return false if File.directory?(path) return false if File.directory?(path)
info = EntryInfoHeader.new(0, 0, File.size(path)) entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
@entries << Entry.new(name: File.basename(path), path: path, info: info) add_entry(entry: entry, replace: replace)
true
end end
def add_blob(path:, blob:) def add_blob(path:, blob:, replace: false)
info = EntryInfoHeader.new(0, 0, blob.size) info = EntryInfoHeader.new(0, 0, blob.size)
@entries << Entry.new(name: File.basename(path), path: path, info: info, blob: blob) entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
into.crc32 = @entries.last.calculate_crc32 into.crc32 = @entries.last.calculate_crc32
true add_entry(entry: entry, replace: replace)
end end
def add_entry(entry:) def add_entry(entry:, replace: false)
@entries << entry duplicate = @entries.find { |e| e.name.upcase == entry.name.upcase }
if duplicate
if replace
@entries.delete(duplicate)
else
return false
end
end
@entries << entry
true true
end end