97 Commits

Author SHA1 Message Date
d92a8753d8 Bump version 2025-10-31 11:54:28 -05:00
b299593076 Fixed a couple edge cases with Task#normalize_path causing failures 2025-10-31 11:53:53 -05:00
ce10cdc658 Fix not properly saving access_token_expiry value 2025-10-25 21:23:55 -05:00
5a3f350015 Fixed edge case where Task#normalize_path wouldn't handle partial matches of correctly 2025-10-24 23:05:04 -05:00
d53299e904 Windows is too easy, or annoying. Fix Task#normalize_path on windows ignoring base_path" 2025-10-15 16:27:28 -05:00
d12d3ff6b8 Don't downcase file path unless we need too, update gems. 2025-10-10 13:30:37 -05:00
d67ffa14a3 Show error message at start up if we cannot resolve critical domains. Fixes #15 2025-10-08 15:12:22 -05:00
71047ce9e8 Ugg. 2025-10-08 14:16:33 -05:00
7da716dde4 Possibly fix failing to rescue from networking errors when fetching packages 2025-10-08 14:09:11 -05:00
3a72a2e094 Possibly fix failing to rescue from timeouts 2025-10-08 13:51:32 -05:00
3c565e6fee Bump version 2025-10-08 11:33:05 -05:00
2dc750a686 Task#normalize_path now takes the base path as an argument and attempts to find the case-insensitive file path to target path 2025-10-08 11:32:35 -05:00
ed119a4925 Fixed All Games section was failing to load app icons 2025-09-19 13:55:06 -05:00
e4d99aac00 Added functional support for developer multi join 2025-09-12 23:41:03 -05:00
e9b8638c27 Fixed (soft) crash when downloading package with a space in its name 2025-09-05 20:02:58 -05:00
4997cfabb0 Fixed not handling filename case for patches 2025-08-26 09:23:54 -05:00
The Unnamed Engineer
0c906464f0 case desensitize unzip 2025-08-26 08:55:27 -05:00
The Unnamed Engineer
5bafc77d97 Update task.rb
Modify all potentially case sensitive file operations to operate in a case-insensitive manner.
2025-08-26 08:55:27 -05:00
30aa44312d Fixed failing to download application manifests unless logged in by checking which source the application/channel orginated from, updated gems. 2025-08-26 08:51:08 -05:00
2031f589b7 Moved processed app icons to cache directory, removed app logos (banners) from media since we now download them and app backgrounds from the api 2025-08-04 22:09:00 -05:00
b909952790 Use fresh logos and backgrounds 2025-08-04 12:25:27 -05:00
6d651c7ad6 Download game logos and backgrounds from backend 2025-08-04 12:25:13 -05:00
60909b0963 Fixed W3DHub.ask_folder crashing on windows 2025-08-04 12:18:38 -05:00
48617b26da Minor post-merge refactor, mainly moved duplicated method ca_bundle_path into common.rb 2025-08-04 10:50:07 -05:00
ad2544a56b Merge branch 'The-Unnamed-Engineer-feature-buildBinaryPackage' 2025-08-03 17:10:06 -05:00
80c104772f Merge branch 'feature-buildBinaryPackage' of github.com:The-Unnamed-Engineer/w3d_hub_linux_launcher into The-Unnamed-Engineer-feature-buildBinaryPackage 2025-08-03 17:09:36 -05:00
09082c0c5d Whitespace 2025-08-03 17:09:10 -05:00
The Unnamed Engineer
27e5da9fd2 Merge branch 'cyberarm:master' into feature-buildBinaryPackage 2025-08-03 17:55:46 -04:00
0bb8ef5f19 Initial work launcher (self) updater 2025-06-25 19:45:23 -05:00
cc0910e68e Updated cyberarm_engine gem to fix edit_line's with prefilled text not visible 2025-06-24 13:56:50 -05:00
fd728fa945 Redid Settings page 2025-06-24 13:47:02 -05:00
ec6dfe8371 Bump version 2025-06-24 10:41:07 -05:00
49d501a8b0 Refactored API to support both backends and to re-enable logging in (on the primary backend) 2025-06-24 10:38:41 -05:00
The Unnamed Engineer
e239f9cd4d Patch IRC config to detect RHEL cert bundle 2025-06-11 06:21:11 -04:00
The Unnamed Engineer
b68d24deda Simply warn for unknown languages. 2025-06-10 15:02:23 -04:00
The Unnamed Engineer
1081832df0 Handle cases where image has not yet downloaded 2025-06-10 13:45:01 -04:00
The Unnamed Engineer
c3cee78265 Fix language error crash 2025-06-10 13:22:24 -04:00
The Unnamed Engineer
4d3163740a Update API to support RHEL cert bundle 2025-06-10 13:21:34 -04:00
The Unnamed Engineer
f1953c45e7 Initial support for binary packaging 2025-06-10 11:20:55 -04:00
685a1aa82c Bump version 2025-05-16 09:48:13 -05:00
9dfee9d1d3 Updated server browser to order servers by player count, then by ping. 2025-05-16 09:47:49 -05:00
1e0adc398c Add support for patching encrypted mixes 2025-05-16 09:39:05 -05:00
3485d5b61a Bump version 2025-04-26 19:46:17 -05:00
cb81a51bfe Fixed failing to fetch manifests properly 2025-04-26 19:45:58 -05:00
314201f238 Switch server list to alternate 2025-04-26 09:55:06 -05:00
pure_bliss
12721cbfbc Fixed newline on end of file 2025-04-26 09:53:50 -05:00
pure_bliss
5ef11fbee8 Not showing image in news if it failed to fetch 2025-04-26 09:53:50 -05:00
e73abce65e Bump version 2025-04-26 07:52:53 -05:00
9b1cb1bb95 Make Task check status of package download instead of assuming it succeeded 2025-04-26 07:52:25 -05:00
c9185e9859 Added download_url support to Package 2025-04-26 07:51:37 -05:00
e4a0d2a848 Fixed news parser a little to not fail if it gets an empty json hash 2025-04-26 07:51:13 -05:00
1401b80057 Fixed status of package downloader not checked, package downloader now supports download_url field from the new backend 2025-04-26 07:49:14 -05:00
cfae4ec3a5 Update to use new backend, package downloader will follow redirects 2025-04-26 07:18:36 -05:00
c344e6a522 Fixed crash at startup when there is no data cached for applications. Fixes #10 2025-04-26 07:16:22 -05:00
696c30aa63 Don't attempt to generate app icon if the package's details has an error 2025-04-23 23:34:39 -05:00
1818d8bec9 Fixed error preventing GSH client from working, moved server list starting task ahead of other tasks to ensure server list _can_ populate if other parts of the backend are dead 2025-04-23 22:03:16 -05:00
4af10a998e Update gems 2025-01-10 12:42:25 -06:00
6736abc277 Write data/paths.ini file 2024-09-11 16:54:21 -05:00
c9c5e18d70 Update theme: testing and dangerous buttons now have slight gradient 2024-04-04 15:07:40 -05:00
67c52c84a1 Removed hardcoded GSH endpoint from signalr server list updater, moved direct connect button to the right of the server list, added a slight gradient to buttons so their not totally flat. 2024-03-17 11:29:39 -05:00
80d1fa865c Fixed auto selected server not checking if full, fixed game icon on bar only showing update available icon for 'first' channel 2024-03-14 11:34:53 -05:00
a1810e3f2c Cache application data for offline use 2024-03-12 11:24:44 -05:00
75b9e3e14a Bump version 2024-03-12 10:43:17 -05:00
7fdb406588 Fix handling of extended-data in offline mode 2024-03-12 10:42:10 -05:00
0ab616f48b Bump version 2024-03-12 09:31:16 -05:00
e035b1ed58 Hide nonfunctional game modifications tab 2024-03-12 09:30:51 -05:00
3f7ec2fb5c Partially revert ask_file/folder on linux to explicitly use zenity/kdialog commands directly instead of using libui hack which leaves the file browser window open after returning 2024-03-12 09:21:52 -05:00
6d209c8942 Server and applications lists are now updated every 5 and 10 minutes respectively 2024-03-11 22:35:30 -05:00
f55924596d Fix some weird scoping issues with ServerListUpdater lambdas 2024-03-11 19:17:38 -05:00
d84c8321c5 Speed up API requests be using a persistent connection, increase news fetch buffer time to 30 seconds from 10 seconds. 2024-03-11 15:09:31 -05:00
38e0de76df Update community news every hour, added 10 second delay between fetch attempts to prevent making a bunch of unneeded requests 2024-03-11 14:16:33 -05:00
9bdca9eba1 Make game news expire after an hour and get refetched 2024-03-11 14:04:03 -05:00
02307f1789 Ducktape on libui for native file open dialogs 2024-03-11 13:45:10 -05:00
c1ca3ec80e Added libui to gems list to use in future for its open_folder dialogs, improvements to server list updater and server browser to correctly handle unregistering and registering servers 2024-03-11 13:09:05 -05:00
29c8667602 Improve error messaging for fetching packages 2024-03-11 13:06:50 -05:00
b594cdae96 Bump version 2024-03-05 13:23:21 -06:00
d350e51d0b Improvements to server browser 2024-03-05 12:50:51 -06:00
0cbe013a11 Use new menu and menu_item elements instead of custom extension, improved styling of list_box menus in game settings 2024-03-04 20:57:13 -06:00
5c806852a5 Store game colour and uses_engine_cfg to settings config, use wwconfig when game doesn't use engine.cfg 2024-03-04 18:17:32 -06:00
655fc14557 Handle user not having an avatar image 2024-03-04 17:34:55 -06:00
6772d4757f Bump version 2024-03-04 16:36:50 -06:00
3383cbd019 Make import button gooder, explicitly require excon http client 2024-03-04 16:35:52 -06:00
aceed86cb4 Fixed checkbox on favorites tab clipping buttom border 2024-03-02 12:14:53 -06:00
58daeffb14 Fixed renegade not importing from registry 2024-03-02 12:13:34 -06:00
2a4bb87e68 Bump version 2024-03-02 11:19:20 -06:00
fc643235b5 Fixed Task#verify_files, patching will now actually delete removed files now, some minor refactoring. 2024-03-02 10:56:58 -06:00
cd0db4e0fc Refactored Task#build_package_list to fix 'overdownloading' packages that then are replaced/no longer needed; FIXME: broke Repair task in the process. 2024-03-02 01:41:36 -06:00
84051103fc Improved avatar handling, fixed import/install button text sizes, fixed typo ERRNO-> Errno, ocra[n] packaging adjustments 2024-03-01 22:39:18 -06:00
f2dd844181 Tweak download manager 2024-03-01 20:25:49 -06:00
a512669a2d UX improvements to Server Browser 2024-02-29 16:36:06 -06:00
85d408fad7 More style updates 2024-02-29 13:54:30 -06:00
d4e81dd441 Server profiles can now be deleted from Asterisk and game_title is now correctly saved to server profile when first created. 2024-02-28 21:47:41 -06:00
0d1333ee4f Update gems 2024-02-28 20:44:32 -06:00
458a9e8832 Bump version 2024-02-28 19:28:29 -06:00
924f4c2b75 More UI tweaks, prettied up Boot state, Updated Welcome state (may be I'll use it someday? 😁) 2024-02-28 19:23:01 -06:00
0b9b519848 More styling changes 2024-02-28 18:14:41 -06:00
f9d401e713 Styling improvements 2024-02-28 10:52:29 -06:00
57 changed files with 2050 additions and 998 deletions

47
.github/workflows/build-tebako.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Build Launcher Binary
on:
push:
branches: [ master, test ]
workflow_dispatch:
jobs:
build-tebako:
runs-on: ubuntu-latest
strategy:
matrix:
architecture: [x64]
container:
image: ghcr.io/tamatebako/tebako-ubuntu-20.04:latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Install Gosu and native dependencies
run: |
apt-get update
apt-get install -y libsdl2-dev libgl1-mesa-dev libopenal-dev libgmp-dev libfontconfig1-dev libsndfile1-dev libmpg123-dev libpango1.0-dev libtool libssl-dev libffi-dev
- name: Update Bundler and lockfile
run: |
gem install bundler -v 2.4.22
bundle _2.4.22_ lock --update --bundler
- name: Build Tebako binary
run: |
tebako press -P -R 3.4.1 -m bundle -o w3d_hub_linux_launcher -r $PWD -e w3d_hub_linux_launcher.rb
- name: Prepare artifact directory
run: |
mkdir w3d-hub-launcher-x86_64
cp w3d_hub_linux_launcher w3d-hub-launcher-x86_64/
cp -r media w3d-hub-launcher-x86_64/
cp -r locales w3d-hub-launcher-x86_64/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: w3d-hub-launcher-x86_64
path: w3d-hub-launcher-x86_64

21
Gemfile
View File

@@ -1,18 +1,27 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "base64" gem "base64"
gem "excon"
gem "cyberarm_engine" gem "cyberarm_engine"
gem "sdl2-bindings" gem "sdl2-bindings"
gem "libui", platforms: [:windows]
gem "digest-crc" gem "digest-crc"
gem "i18n" gem "i18n"
gem "ircparser" gem "ircparser"
gem "rexml" gem "rexml"
gem "rubyzip" gem "rubyzip"
gem "websocket-client-simple" gem "websocket-client-simple"
gem "win32-process", platforms: [:x64_mingw, :mingw] gem "win32-process", platforms: [:windows]
gem "win32-security", platforms: [:x64_mingw, :mingw] gem "win32-security", platforms: [:windows]
# group :windows_packaging do # PACKAGING NOTES
# gem "rake" # bundler 2.5.x doesn't seem to play nice with ocra[n]
# gem "releasy" # use `bundle _x.y.z_ COMMAND` to use this one...
# end # 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
# group :windows_packaging do
# gem "bundler", "~>2.4.3"
# gem "rake"
# gem "ocran"
# gem "releasy"#, path: "../releasy"
# end

View File

@@ -1,32 +1,38 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
base64 (0.2.0) base64 (0.3.0)
concurrent-ruby (1.2.3) concurrent-ruby (1.3.5)
cyberarm_engine (0.24.1) cyberarm_engine (0.24.5)
excon (~> 0.88)
gosu (~> 1.1) gosu (~> 1.1)
gosu_more_drawables (~> 0.3) digest-crc (0.7.0)
digest-crc (0.6.5)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
event_emitter (0.2.6) event_emitter (0.2.6)
excon (0.109.0) excon (1.3.0)
ffi (1.16.3) logger
ffi (1.17.2-x64-mingw-ucrt)
ffi (1.17.2-x86_64-linux-gnu)
ffi-win32-extensions (1.0.4) ffi-win32-extensions (1.0.4)
ffi ffi
fiddle (1.1.8)
gosu (1.4.6) gosu (1.4.6)
gosu_more_drawables (0.3.1) i18n (1.14.7)
i18n (1.14.1)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
ircparser (1.0.0) ircparser (1.0.0)
rake (13.1.0) libui (0.2.0-x64-mingw-ucrt)
rexml (3.2.6) fiddle
rubyzip (2.3.2) logger (1.7.0)
mutex_m (0.3.0)
rake (13.3.0)
rexml (3.4.4)
rubyzip (3.1.1)
sdl2-bindings (0.2.3) sdl2-bindings (0.2.3)
ffi (~> 1.15) ffi (~> 1.15)
websocket (1.2.10) websocket (1.2.11)
websocket-client-simple (0.8.0) websocket-client-simple (0.9.0)
base64
event_emitter event_emitter
mutex_m
websocket websocket
win32-process (0.10.0) win32-process (0.10.0)
ffi (>= 1.0.0) ffi (>= 1.0.0)
@@ -36,15 +42,16 @@ GEM
PLATFORMS PLATFORMS
x64-mingw-ucrt x64-mingw-ucrt
x64-mingw32
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
base64 base64
cyberarm_engine cyberarm_engine
digest-crc digest-crc
excon
i18n i18n
ircparser ircparser
libui
rexml rexml
rubyzip rubyzip
sdl2-bindings sdl2-bindings
@@ -53,4 +60,4 @@ DEPENDENCIES
win32-security win32-security
BUNDLED WITH BUNDLED WITH
2.5.3 2.6.8

View File

@@ -11,13 +11,13 @@ Releasy::Project.new do
version W3DHub::VERSION version W3DHub::VERSION
executable "w3d_hub_linux_launcher.rb" executable "w3d_hub_linux_launcher.rb"
files ["lib/**/*.*", "locales/*", "media/**/**", "data/.gitkeep", "data/cache/.gitkeep"] files ["lib/**/*.*", "locales/*", "media/**/**", "data/.gitkeep", "data/cache/.gitkeep", "data/logs/.gitkeep"]
exclude_encoding # Applications that don't use advanced encoding (e.g. Japanese characters) can save build size with this. # exclude_encoding # Applications that don't use advanced encoding (e.g. Japanese characters) can save build size with this.
verbose verbose
add_build :windows_folder do add_build :windows_folder do
icon "media/icons/app.ico" icon "media/icons/app.ico"
executable_type :console # Assuming you don't want it to run with a console window. executable_type :windows # :console # Assuming you don't want it to run with a console window.
add_package :exe # Windows self-extracting archive. add_package :exe # Windows self-extracting archive.
end end
end end

View File

@@ -1,8 +1,14 @@
class W3DHub class W3DHub
class Api class Api
# Set Excon default CA file if found
if (ca_file = W3DHub.ca_bundle_path)
Excon.defaults[:ssl_ca_file] = ca_file
end
LOG_TAG = "W3DHub::Api".freeze LOG_TAG = "W3DHub::Api".freeze
API_TIMEOUT = 10 # seconds API_TIMEOUT = 30 # seconds
USER_AGENT = "Cyberarm's Linux Friendly W3D Hub Launcher v#{W3DHub::VERSION}".freeze USER_AGENT = "Cyberarm's Linux Friendly W3D Hub Launcher v#{W3DHub::VERSION}".freeze
DEFAULT_HEADERS = { DEFAULT_HEADERS = {
"User-Agent": USER_AGENT, "User-Agent": USER_AGENT,
@@ -41,11 +47,26 @@ class W3DHub
end end
#! === W3D Hub API === !# #! === 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://secure.w3dhub.com".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_API_CONNECTION = Excon.new(ALT_W3DHUB_API_ENDPOINT, persistent: true)
def self.excon(method, url, headers = DEFAULT_HEADERS, body = nil) def self.excon(method, url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." } 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 # Inject Authorization header if account data is populated
if Store.account if Store.account
@@ -55,8 +76,52 @@ class W3DHub
end end
begin begin
Excon.send( connection.send(
method, method,
path: url.sub(endpoint, ""),
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::Error::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
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, url,
headers: headers, headers: headers,
body: body, body: body,
@@ -70,7 +135,7 @@ class W3DHub
retry_interval: 1, retry_interval: 1,
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
) )
rescue Excon::Errors::Timeout => e rescue Excon::Error::Timeout => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" } logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
DummyResponse.new(e) DummyResponse.new(e)
@@ -82,10 +147,6 @@ class W3DHub
end end
end end
def self.post(url, headers = DEFAULT_HEADERS, body = nil)
excon(:post, url, headers, body)
end
# Method: POST # Method: POST
# FORMAT: JSON # FORMAT: JSON
@@ -101,24 +162,16 @@ class W3DHub
# #
# On a failed login the service responds with: # On a failed login the service responds with:
# {"error":"login-failed"} # {"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})}" 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 if response.status == 200
user_data = JSON.parse(response.body, symbolize_names: true) user_data = JSON.parse(response.body, symbolize_names: true)
return false if user_data[:error] return false if user_data[:error]
body = "data=#{JSON.dump({ id: user_data[:userid] })}" user_details_data = user_details(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
Account.new(user_data, user_details_data) Account.new(user_data, user_details_data)
else else
@@ -129,24 +182,16 @@ class W3DHub
end end
# See #user_refresh_token # 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})}" 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 if response.status == 200
user_data = JSON.parse(response.body, symbolize_names: true) user_data = JSON.parse(response.body, symbolize_names: true)
return false if user_data[:error] return false if user_data[:error]
body = "data=#{JSON.dump({ id: user_data[:userid] })}" user_details_data = user_details(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
Account.new(user_data, user_details_data) Account.new(user_data, user_details_data)
else else
@@ -156,18 +201,27 @@ class W3DHub
end end
end end
# /apis/launcher/1/user-login # /apis/w3dhub/1/get-user-details
# Client sends an Authorization header bearer token which is received from logging in (Required?)
# #
# Response: avatar-uri (Image download uri), id, username # 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 end
# /apis/w3dhub/1/get-service-status # /apis/w3dhub/1/get-service-status
# Service response: # Service response:
# {"services":{"authentication":true,"packageDownload":true}} # {"services":{"authentication":true,"packageDownload":true}}
def self.service_status def self.service_status(backend = :w3dhub)
response = post("#{ENDPOINT}/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS) response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend)
if response.status == 200 if response.status == 200
ServiceStatus.new(response.body) ServiceStatus.new(response.body)
@@ -182,11 +236,11 @@ class W3DHub
# Client sends an Authorization header bearer token which is received from logging in (Optional) # Client sends an Authorization header bearer token which is received from logging in (Optional)
# Launcher sends an empty data request: data={} # Launcher sends an empty data request: data={}
# Response is a list of applications/games # Response is a list of applications/games
def self.applications def self.applications(backend = :w3dhub)
response = post("#{ENDPOINT}/apis/launcher/1/get-applications") response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend)
if response.status == 200 if response.status == 200
Applications.new(response.body) Applications.new(response.body, backend)
else else
logger.error(LOG_TAG) { "Failed to fetch applications list:" } logger.error(LOG_TAG) { "Failed to fetch applications list:" }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { response }
@@ -194,13 +248,82 @@ class W3DHub
end end
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 don'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 don'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 # /apis/w3dhub/1/get-news
# Client sends an Authorization header bearer token which is received from logging in (Optional) # 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) # 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 # 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})}" 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 if response.status == 200
News.new(response.body) News.new(response.body)
@@ -216,12 +339,13 @@ class W3DHub
# /apis/launcher/1/get-package-details # /apis/launcher/1/get-package-details
# client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]} # 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 })) 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 if response.status == 200
hash = JSON.parse(response.body, symbolize_names: true) hash = JSON.parse(response.body, symbolize_names: true)
hash[:packages].map { |pkg| Package.new(pkg) } hash[:packages].map { |pkg| Package.new(pkg) }
else else
logger.error(LOG_TAG) { "Failed to fetch package details for:" } logger.error(LOG_TAG) { "Failed to fetch package details for:" }
@@ -242,9 +366,9 @@ class W3DHub
# /apis/w3dhub/1/get-events # /apis/w3dhub/1/get-events
# #
# clients requests events: data={"serverPath":"apb"} # 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 })) 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 if response.status == 200
array = JSON.parse(response.body, symbolize_names: true) array = JSON.parse(response.body, symbolize_names: true)
@@ -256,11 +380,10 @@ class W3DHub
#! === Server List API === !# #! === Server List API === !#
SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze # SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze
SERVER_LIST_ENDPOINT = "https://gsh.w3d.cyberarm.dev".freeze
def self.get(url, headers = DEFAULT_HEADERS, body = nil) # SERVER_LIST_ENDPOINT = "http://127.0.0.1:9292".freeze
excon(:get, url, headers, body) GSH_CONNECTION = Excon.new(SERVER_LIST_ENDPOINT, persistent: true)
end
# Method: GET # Method: GET
# FORMAT: JSON # FORMAT: JSON
@@ -279,8 +402,8 @@ class W3DHub
# id, name, score, kills, deaths # id, name, score, kills, deaths
# ...players[]: # ...players[]:
# nick, team (index of teams array), score, kills, deaths # nick, team (index of teams array), score, kills, deaths
def self.server_list(level = 1) def self.server_list(level = 1, backend = :gsh)
response = get("#{SERVER_LIST_ENDPOINT}/listings/getAll/v2?statusLevel=#{level}") response = get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend)
if response.status == 200 if response.status == 200
data = JSON.parse(response.body, symbolize_names: true) data = JSON.parse(response.body, symbolize_names: true)
@@ -301,8 +424,10 @@ class W3DHub
# id, name, score, kills, deaths # id, name, score, kills, deaths
# ...players[]: # ...players[]:
# nick, team (index of teams array), score, kills, deaths # nick, team (index of teams array), score, kills, deaths
def self.server_details(id, level) def self.server_details(id, level, backend = :gsh)
response = get("#{SERVER_LIST_ENDPOINT}/listings/getStatus/v2/#{id}?statusLevel=#{level}") return false unless id && level
response = get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend)
if response.status == 200 if response.status == 200
hash = JSON.parse(response.body, symbolize_names: true) hash = JSON.parse(response.body, symbolize_names: true)

View File

@@ -24,8 +24,9 @@ class W3DHub
def to_json(env) def to_json(env)
d = @data.dup d = @data.dup
d[:avatar_uri] = @avatar_uri d[:avatar_uri] = @avatar_uri
d[:access_token_expiry] = d[:access_token_expiry].to_i d[:access_token_expiry] = @access_token_expiry.to_i
d.to_json(env) d.to_json(env)
end end

View File

@@ -1,14 +1,16 @@
class W3DHub class W3DHub
class Api class Api
class Applications class Applications
def initialize(response) attr_reader :data
def initialize(response, source = nil)
@data = JSON.parse(response, symbolize_names: true) @data = JSON.parse(response, symbolize_names: true)
games = @data[:applications].select { |a| a[:category] == "games" } games = @data[:applications].select { |a| a[:category] == "games" }
@games = [] @games = []
games.each { |hash| @games << Game.new(hash) } games.each { |hash| @games << Game.new(hash, source) }
@games.sort_by!(&:name).reverse @games.sort_by!(&:name).reverse
end end
@@ -18,37 +20,71 @@ class W3DHub
class Game class Game
attr_reader :id, :name, :type, :category, :studio_id, :channels, :web_links, :color attr_reader :id, :name, :type, :category, :studio_id, :channels, :web_links, :color
attr_reader :___source
def initialize(hash) def initialize(hash, source = nil)
@data = hash @data = hash
@data[:___source] = source if source
@id = @data[:id] @id = @data[:id].to_s
@name = @data[:name] @name = @data[:name]
@type = @data[:type] @type = @data[:type]
@category = @data[:category] @category = @data[:category]
@studio_id = @data[:"studio-id"] @studio_id = @data[:"studio-id"]
# TODO: Do processing # TODO: Do processing
@channels = @data[:channels].map { |channel| Channel.new(channel) } @channels = @data[:channels].map { |channel| Channel.new(channel, source) }
@web_links = @data[:"web-links"]&.map { |link| WebLink.new(link) } || [] @web_links = @data[:"web-links"]&.map { |link| WebLink.new(link) } || []
@extended_data = @data[:"extended-data"] @extended_data = @data[:"extended-data"]
color = @data[:"extended-data"].find { |h| h[:name] == "colour" }[:value].sub("#", "") color = @data[:"extended-data"].find { |h| h[:name] == "colour" }[:value].sub("#", "")
color = color.sub("ff", "") if color.length == 8
@color = "ff#{color}".to_i(16) @color = "ff#{color}".to_i(16)
cfg = @data[:"extended-data"].find { |h| h[:name] == "usesEngineCfg" }
@uses_engine_cfg = (cfg && cfg[:value].to_s.downcase.strip == "true") == true # explicit truthy compare to prevent return `nil`
cfg = @data[:"extended-data"].find { |h| h[:name] == "usesRenFolder" }
@uses_ren_folder = (cfg && cfg[:value].to_s.downcase.strip == "true") == true # explicit truthy compare to prevent return `nil`
end
def uses_engine_cfg?
@uses_engine_cfg
end
def uses_ren_folder?
@uses_ren_folder
end
def source
@data[:___source]&.to_sym || :w3dhub
end
def source=(sym)
@data[:___source] = sym
end end
class Channel class Channel
attr_reader :id, :name, :user_level, :current_version attr_reader :id, :name, :user_level, :current_version
def initialize(hash) def initialize(hash, source = nil)
@data = hash @data = hash
@data[:___source] = source
@id = @data[:id] @id = @data[:id].to_s
@name = @data[:name] @name = @data[:name]
@user_level = @data[:"user-level"] @user_level = @data[:"user-level"]
@current_version = @data[:"current-version"] @current_version = @data[:"current-version"]
end end
def source
@data[:___source]&.to_sym || :w3dhub
end
def source=(sym)
@data[:___source] = sym
end
end end
class WebLink class WebLink

View File

@@ -6,7 +6,7 @@ class W3DHub
def initialize(response) def initialize(response)
@data = JSON.parse(response, symbolize_names: true) @data = JSON.parse(response, symbolize_names: true)
@items = @data[:news].map { |item| Item.new(item) } @items = (@data[:news] && @data[:news].is_a?(Array)) ? @data[:news].map { |item| Item.new(item) } : []
end end
class Item class Item

View File

@@ -1,7 +1,7 @@
class W3DHub class W3DHub
class Api class Api
class Package class Package
attr_reader :category, :subcategory, :name, :version, :size, :checksum, :checksum_chunk_size, :checksum_chunks, attr_reader :category, :subcategory, :name, :version, :size, :checksum, :checksum_chunk_size, :checksum_chunks, :download_url, :error,
:custom_partially_valid_at_bytes, :custom_is_patch :custom_partially_valid_at_bytes, :custom_is_patch
def initialize(hash) def initialize(hash)
@@ -16,6 +16,9 @@ class W3DHub
@checksum = @data[:checksum] @checksum = @data[:checksum]
@checksum_chunk_size = @data[:"checksum-chunk-size"] @checksum_chunk_size = @data[:"checksum-chunk-size"]
@checksum_chunks = @data[:"checksum-chunks"] @checksum_chunks = @data[:"checksum-chunks"]
@error = @data[:error] || nil
@download_url = @data[:download_url] || nil
@custom_partially_valid_at_bytes = 0 @custom_partially_valid_at_bytes = 0
@custom_is_patch = false @custom_is_patch = false
@@ -25,6 +28,10 @@ class W3DHub
@checksum_chunks[:"#{key}"] @checksum_chunks[:"#{key}"]
end end
def error?
@error
end
def partially_valid_at_bytes=(i) def partially_valid_at_bytes=(i)
@custom_partially_valid_at_bytes = i @custom_partially_valid_at_bytes = i
end end

View File

@@ -1,6 +1,8 @@
class W3DHub class W3DHub
class Api class Api
class ServerListServer class ServerListServer
NO_OR_BAD_PING = 1_000_000
attr_reader :id, :game, :address, :port, :region, :channel, :ping, :status attr_reader :id, :game, :address, :port, :region, :channel, :ping, :status
def initialize(hash) def initialize(hash)
@@ -12,33 +14,33 @@ class W3DHub
@port = @data[:port] @port = @data[:port]
@region = @data[:region] @region = @data[:region]
@channel = @data[:channel] || "release" @channel = @data[:channel] || "release"
@ping = -1 @ping = NO_OR_BAD_PING
@status = @data[:status] ? Status.new(@data[:status]) : nil @status = Status.new(@data[:status])
@ping_interval = 30_000 @ping_interval = 30_000
@last_pinged = Gosu.milliseconds + @ping_interval + 1 @last_pinged = Gosu.milliseconds + @ping_interval + 1_000
end end
def update(hash) def update(hash)
if @status if @status
@status.instance_variable_set(:@name, hash[:name]) @status.name = hash[:name]
@status.instance_variable_set(:@password, hash[:password] || false) @status.password = hash[:password] || false
@status.instance_variable_set(:@map, hash[:map]) @status.map = hash[:map]
@status.instance_variable_set(:@max_players, hash[:maxplayers]) @status.max_players = hash[:maxplayers]
@status.instance_variable_set(:@player_count, hash[:numplayers] || 0) @status.player_count = hash[:numplayers] || 0
@status.instance_variable_set(:@started, hash[:started]) @status.started = hash[:started]
@status.instance_variable_set(:@remaining, hash[:remaining]) @status.remaining = hash[:remaining]
@status.instance_variable_set(:@teams, hash[:teams]&.map { |t| Team.new(t) }) if hash[:teams] @status.teams = hash[:teams]&.map { |t| Team.new(t) } if hash[:teams]
@status.instance_variable_set(:@players, hash[:players]&.select { |t| t[:nick] != "Nod" && t[:nick] != "GDI" }&.map { |t| Player.new(t) }) if hash[:players] @status.players = hash[:players]&.select { |t| t[:nick] != "Nod" && t[:nick] != "GDI" }&.map { |t| Player.new(t) } if hash[:players]
send_ping send_ping
else
return true @status = Status.new(hash)
end end
false true
end end
def send_ping(force_ping = false) def send_ping(force_ping = false)
@@ -47,8 +49,6 @@ class W3DHub
W3DHub::BackgroundWorker.foreground_parallel_job( W3DHub::BackgroundWorker.foreground_parallel_job(
lambda do lambda do
@ping = -1
W3DHub.command("ping #{@address} #{W3DHub.windows? ? '-n 3' : '-c 3'}") do |line| W3DHub.command("ping #{@address} #{W3DHub.windows? ? '-n 3' : '-c 3'}") do |line|
if W3DHub.windows? && line =~ /Minimum|Maximum|Maximum/i if W3DHub.windows? && line =~ /Minimum|Maximum|Maximum/i
@ping = line.strip.split(",").last.split("=").last.sub("ms", "").to_i @ping = line.strip.split(",").last.split("=").last.sub("ms", "").to_i
@@ -57,6 +57,8 @@ class W3DHub
end end
end end
@ping = NO_OR_BAD_PING if @ping.zero?
@ping @ping
end, end,
lambda do |_| lambda do |_|
@@ -67,26 +69,26 @@ class W3DHub
end end
class Status class Status
attr_reader :name, :password, :map, :max_players, :player_count, :started, :remaining, :teams, :players attr_accessor :name, :password, :map, :max_players, :player_count, :started, :remaining, :teams, :players
def initialize(hash) def initialize(hash)
@data = hash @data = hash || {}
@teams = @data[:teams]&.map { |t| Team.new(t) } @teams = @data[:teams]&.map { |t| Team.new(t) } || []
@players = @data[:players]&.select { |t| t[:nick] != "Nod" && t[:nick] != "GDI" }&.map { |t| Player.new(t) } @players = @data[:players]&.select { |t| t[:nick] != "Nod" && t[:nick] != "GDI" }&.map { |t| Player.new(t) } || []
@name = @data[:name] @name = @data[:name] || ""
@password = @data[:password] || false @password = @data[:password] || false
@map = @data[:map] @map = @data[:map] || ""
@max_players = @data[:maxplayers] @max_players = @data[:maxplayers] || 0
@player_count = @players.size || @data[:numplayers].to_i @player_count = @players.size || @data[:numplayers].to_i
@started = @data[:started] @started = @data[:started] || Time.now
@remaining = @data[:remaining] @remaining = @data[:remaining] || "00.00.00"
end end
end end
class Team class Team
attr_reader :id, :name, :score, :kills, :deaths attr_accessor :id, :name, :score, :kills, :deaths
def initialize(hash) def initialize(hash)
@data = hash @data = hash
@@ -100,7 +102,7 @@ class W3DHub
end end
class Player class Player
attr_reader :nick, :team, :score, :kills, :deaths attr_accessor :nick, :team, :score, :kills, :deaths
def initialize(hash) def initialize(hash)
@data = hash @data = hash

View File

@@ -11,11 +11,13 @@ class W3DHub
@@instance = ServerListUpdater.new @@instance = ServerListUpdater.new
end end
attr_accessor :auto_reconnect attr_accessor :auto_reconnect, :invocation_id
def initialize def initialize
@auto_reconnect = false @auto_reconnect = false
@invocation_id = 0
logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." } logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." }
run run
end end
@@ -33,7 +35,7 @@ class W3DHub
puts e puts e
puts e.backtrace puts e.backtrace
sleep 10 sleep 30
retry retry
end end
end end
@@ -46,24 +48,31 @@ class W3DHub
@auto_reconnect = false @auto_reconnect = false
logger.debug(LOG_TAG) { "Requesting connection token..." } logger.debug(LOG_TAG) { "Requesting connection token..." }
response = Excon.post("https://gsh.w3dhub.com/listings/push/v2/negotiate?negotiateVersion=1", headers: Api::DEFAULT_HEADERS, body: "") response = Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh)
if response.status != 200
@auto_reconnect = true
return
end
data = JSON.parse(response.body, symbolize_names: true) data = JSON.parse(response.body, symbolize_names: true)
@invocation_id = 0 if @invocation_id > 9095
id = data[:connectionToken] id = data[:connectionToken]
endpoint = "https://gsh.w3dhub.com/listings/push/v2?id=#{id}" endpoint = "#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2?id=#{id}"
logger.debug(LOG_TAG) { "Connecting to websocket..." } logger.debug(LOG_TAG) { "Connecting to websocket..." }
this = self this = self
WebSocket::Client::Simple.connect(endpoint, headers: Api::DEFAULT_HEADERS) do |ws| @ws = WebSocket::Client::Simple.connect(endpoint, headers: Api::DEFAULT_HEADERS) do |ws|
ws.on(:open) do ws.on(:open) do
logger.debug(LOG_TAG) { "Requesting json protocol, v1..." } logger.debug(LOG_TAG) { "Requesting json protocol, v1..." }
ws.send({ protocol: "json", version: 1 }.to_json + "\x1e") ws.send({ protocol: "json", version: 1 }.to_json + "\x1e")
logger.debug(LOG_TAG) { "Subscribing to server changes..." } logger.debug(LOG_TAG) { "Subscribing to server changes..." }
Store.server_list.each_with_index do |server, i| Store.server_list.each do |server|
i += 1 this.invocation_id += 1
mode = 1 # 2 full details, 1 basic details mode = 1 # 2 full details, 1 basic details
out = { "type": 1, "invocationId": "#{i}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, mode] } out = { "type": 1, "invocationId": "#{this.invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, mode] }
ws.send(out.to_json + "\x1e") ws.send(out.to_json + "\x1e")
end end
end end
@@ -73,35 +82,119 @@ class W3DHub
hash = JSON.parse(msg, symbolize_names: true) hash = JSON.parse(msg, symbolize_names: true)
# pp hash if hash[:target] != "ServerStatusChanged" && hash[:type] != 6 && hash[:type] != 3
# Send PING(?) # Send PING(?)
if hash.empty? || hash[:type] == 6 if hash.empty? || hash[:type] == 6
ws.send({ type: 6 }.to_json + "\x1e") ws.send({ type: 6 }.to_json + "\x1e")
else else
case hash[:type] case hash[:type]
when 1 when 1
if hash[:target] == "ServerStatusChanged" case hash[:target]
when "ServerRegistered"
data = hash[:arguments].first
this.invocation_id += 1
out = { "type": 1, "invocationId": "#{this.invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [data[:id], 1] }
ws.send(out.to_json + "\x1e")
BackgroundWorker.foreground_job(
->(data) { [Api.server_details(data[:id], 2), data] },
->(array) do
server_data, data = array
next unless server_data
data[:status] = server_data
server = ServerListServer.new(data)
Store.server_list.push(server)
States::Interface.instance&.update_server_browser(server, :update)
end,
nil,
data
)
when "ServerStatusChanged"
id, data = hash[:arguments] id, data = hash[:arguments]
server = Store.server_list.find { |s| s.id == id } server = Store.server_list.find { |s| s.id == id }
server_updated = server&.update(data) server_updated = server&.update(data)
BackgroundWorker.foreground_job(-> {}, ->(result){ States::Interface.instance&.update_server_browser(server) }) if server_updated BackgroundWorker.foreground_job(->(server) { server }, ->(server) { States::Interface.instance&.update_server_browser(server, :update) }, nil, server) if server_updated
when "ServerUnregistered"
id = hash[:arguments].first
server = Store.server_list.find { |s| s.id == id }
if server
Store.server_list.delete(server)
BackgroundWorker.foreground_job(->(server) { server }, ->(server) { States::Interface.instance&.update_server_browser(server, :remove) }, nil, server)
end
end end
end end
end end
end end
ws.on(:close) do |e| ws.on(:close) do |e|
p e logger.error(LOG_TAG) { e }
this.auto_reconnect = true this.auto_reconnect = true
ws.close ws.close
end end
ws.on(:error) do |e| ws.on(:error) do |e|
p e logger.error(LOG_TAG) { e }
this.auto_reconnect = true this.auto_reconnect = true
ws.close ws.close
end end
end end
@ws = nil
end
def refresh_server_list(list)
new_servers = []
removed_servers = []
# find new servers
list.each do |server|
found_server = Store.server_list.find { |s| s.id == server.id }
new_servers << server unless found_server
end
# find removed servers
Store.server_list.each do |server|
found_server = list.find { |s| s.id == server.id }
removed_servers << server unless found_server
end
# purge removed servers from list
Store.server_list.reject! do |server|
removed_servers.find { |s| server.id == s.id }
end
# add new servers to list
Store.server_list = Store.server_list + new_servers
if @ws
# unsubscribe from removed servers
removed_servers.each do
@invocation_id += 1
out = { "type": 1, "invocationId": "#{@invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, 0] }
ws.send(out.to_json + "\x1e")
end
# subscribe to new servers
new_servers.each do
@invocation_id += 1
out = { "type": 1, "invocationId": "#{@invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, 1] }
ws.send(out.to_json + "\x1e")
end
end
# sort list
Store.server_list.sort_by! { |s| [s.status.player_count, s.id] }.reverse!
end end
end end
end end

View File

@@ -44,7 +44,7 @@ class W3DHub
# if auto-import fails ask user for path to game exe # if auto-import fails ask user for path to game exe
# mark app as imported/installed # mark app as imported/installed
@tasks.push(Importer.new(app_id, channel)) push_state(W3DHub::States::ImportGameDialog, app_id: app_id, channel: channel)
end end
def settings(app_id, channel) def settings(app_id, channel)
@@ -201,18 +201,45 @@ class W3DHub
end end
end end
def run(app_id, channel, *args) def start_command(path, exe)
if (app_data = installed?(app_id, channel)) if W3DHub.windows?
pid = Process.spawn("#{dxvk_command(app_id, channel)}#{mangohud_command(app_id, channel)}#{wine_command(app_id, channel)}\"#{app_data[:install_path]}\" -launcher #{args.join(' ')}") "start /D \"#{path}\" /B #{exe}"
Process.detach(pid) else
"#{path}/#{exe}"
end end
end end
def join_server(app_id, channel, server, password = nil) def run(app_id, channel, *args)
if installed?(app_id, channel) && Store.settings[:server_list_username].to_s.length.positive? if (app_data = installed?(app_id, channel))
install_directory = app_data[:install_directory]
exe_path = app_id == "ecw" ? "#{install_directory}/game500.exe" : "#{install_directory}/game.exe"
exe_path.gsub!("/", "\\") if W3DHub.windows?
exe_path.gsub!("\\", "/") if W3DHub.unix?
exe = File.basename(exe_path)
path = File.dirname(exe_path)
attempted = false
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(' ')}")
Process.detach(pid)
rescue Errno::EINVAL => e
retryable = !attempted
attempted = true
# Assume that we're on windoze and that the game requires admin
retry if retryable
# TODO: Show an error message if we reach here...
end
end
end
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?
run( run(
app_id, channel, app_id, channel,
"+connect #{server.address}:#{server.port} +netplayername #{Store.settings[:server_list_username]}#{password ? " +password \"#{password}\"" : ""}" "+connect #{server.address}:#{server.port} +netplayername #{username}#{password ? " +password \"#{password}\"" : ""}#{multi ? " +multi" : ""}"
) )
end end
end end
@@ -223,7 +250,7 @@ class W3DHub
return nil unless app_data return nil unless app_data
found_server = Store.server_list.select do |server| found_server = Store.server_list.select do |server|
server.game == app_id && server.channel == channel && !server.status.password server.game == app_id && server.channel == channel && !server.status.password && server.status.player_count < server.status.max_players
end&.first end&.first
found_server ? found_server : nil found_server ? found_server : nil
@@ -314,10 +341,11 @@ class W3DHub
begin begin
reg_constant.open(registry_path, reg_type) do |reg| reg_constant.open(registry_path, reg_type) do |reg|
if (install_path = reg["InstallDir"]) if (install_path = reg["InstallPath"])
install_path = File.dirname(install_path)
install_path.gsub!("\\", "/") install_path.gsub!("\\", "/")
exe_path = app_id == "ecw" ? "#{install_path}/game750.exe" : "#{install_path}/game.exe" exe_path = app_id == "ecw" ? "#{install_path}/game500.exe" : "#{install_path}/game.exe"
if File.exist?(exe_path) if File.exist?(exe_path)
installed_version = app_id == "ren" ? "1.0.0.0" : reg["InstalledVersion"] installed_version = app_id == "ren" ? "1.0.0.0" : reg["InstalledVersion"]
@@ -383,17 +411,19 @@ class W3DHub
false false
end end
def imported!(task, exe_path) def imported!(application, channel, exe_path)
exe_path.gsub!("\\", "/")
application_data = { application_data = {
name: task.application.name, name: application.name,
install_directory: File.dirname(exe_path), install_directory: File.dirname(exe_path),
installed_version: task.channel.current_version, installed_version: channel.current_version,
install_path: exe_path, install_path: exe_path,
wine_prefix: task.wine_prefix wine_prefix: nil
} }
Store.settings[:games] ||= {} Store.settings[:games] ||= {}
Store.settings[:games][:"#{task.app_id}_#{task.release_channel}"] = application_data Store.settings[:games][:"#{application.id}_#{channel.id}"] = application_data
Store.settings.save_settings Store.settings.save_settings
end end
@@ -404,6 +434,8 @@ class W3DHub
# wine_prefix # optional # wine_prefix # optional
install_directory = Cache.install_path(task.application, task.channel) install_directory = Cache.install_path(task.application, task.channel)
install_directory.gsub!("\\", "/")
application_data = { application_data = {
name: task.application.name, name: task.application.name,
install_directory: install_directory, install_directory: install_directory,
@@ -459,6 +491,24 @@ class W3DHub
Store.applications.games.detect { |g| g.id == app_id }&.name Store.applications.games.detect { |g| g.id == app_id }&.name
end end
def channel_name(app_id, channel_id)
app = Store.applications.games.detect { |g| g.id.to_s == app_id.to_s }
return unless app
app.channels.detect { |g| g.id.to_s == channel_id.to_s }&.name
end
def application(app_id)
Store.applications.games.detect { |g| g.id.to_s == app_id.to_s }
end
def channel(app_id, channel_id)
app = Store.applications.games.detect { |g| g.id.to_s == app_id.to_s }
return unless app
app.channels.detect { |g| g.id.to_s == channel_id.to_s }
end
# No application tasks are being done # No application tasks are being done
def idle? def idle?
!busy? !busy?

View File

@@ -30,7 +30,7 @@ class W3DHub
def parse_files def parse_files
@document.root.elements.each("//File") do |element| @document.root.elements.each("//File") do |element|
@files.push(ManifestFile.new(element)) @files.push(ManifestFile.new(element, @version))
end end
end end
@@ -42,9 +42,9 @@ class W3DHub
# TODO: Support patches # TODO: Support patches
class ManifestFile class ManifestFile
attr_reader :name, :checksum, :package, :removed_since, :from attr_reader :name, :checksum, :package, :removed_since, :from, :version
def initialize(xml) def initialize(xml, version)
@data = xml @data = xml
@name = @data["name"] @name = @data["name"]
@@ -58,6 +58,8 @@ class W3DHub
@from = patch["from"] @from = patch["from"]
@package = patch["package"] @package = patch["package"]
end end
@version = version
end end
def removed? def removed?

View File

@@ -27,8 +27,9 @@ class W3DHub
@bytes_downloaded = -1 @bytes_downloaded = -1
@manifests = [] @manifests = []
@files = {} @files = []
@packages = [] @packages = []
@deleted_files = [] # TODO: remove removed files
@wine_prefix = nil @wine_prefix = nil
@@ -61,7 +62,7 @@ class W3DHub
status = execute_task status = execute_task
rescue FailFast rescue FailFast
# no-op # no-op
rescue StandardError, ERRNO::EACCES => e rescue StandardError, Errno::EACCES => e
status = false status = false
@task_failure_reason = e.message[0..512] @task_failure_reason = e.message[0..512]
@@ -77,7 +78,7 @@ class W3DHub
@task_state = :complete unless @task_state == :failed @task_state = :complete unless @task_state == :failed
hide_application_taskbar if @task_state == :failed hide_application_taskbar if @task_state == :failed
send_message_dialog(:failure, "Task #{type.inspect} failed for #{@application.name}", @task_failure_reason) if @task_state == :failed && !@fail_silently send_message_dialog(:failure, "#{type.to_s.capitalize} Task failed for #{@application.name}", @task_failure_reason) if @task_state == :failed && !@fail_silently
# end # end
end end
end end
@@ -111,6 +112,38 @@ class W3DHub
@task_state == :failed @task_state == :failed
end end
def normalize_path(path, base_path)
path = path.to_s.gsub("\\", "/")
return "#{base_path}/#{path}" if W3DHub.windows? # Windows is easy, or annoying, depending how you look at it...
constructed_path = base_path
lowercase_full_path = "#{base_path}/#{path}".downcase.strip.freeze
accepted_parts = 0
split_path = path.split("/")
split_path.each do |segment|
Dir.glob("#{constructed_path}/*").each do |part|
next unless "#{constructed_path}/#{segment}".downcase == part.downcase
# Handle edge case where a file with the same name is in a higher directory
next if File.file?(part) && part.downcase.strip != lowercase_full_path
constructed_path = part
accepted_parts += 1
break
end
end
# Find file if it exists else use provided path as cased
if constructed_path.downcase.strip == lowercase_full_path
constructed_path
elsif accepted_parts.positive?
"#{constructed_path}/#{split_path[accepted_parts..].join('/')}"
else
"#{base_path}/#{path}" # File doesn't exist, case doesn't matter.
end
end
def failure_reason def failure_reason
@task_failure_reason || "" @task_failure_reason || ""
end end
@@ -207,16 +240,23 @@ class W3DHub
@manifests << manifest @manifests << manifest
until(manifest.full?) until(manifest.full?)
fetch_manifest("games", app_id, "manifest.xml", manifest.base_version) if fetch_manifest("games", app_id, "manifest.xml", manifest.base_version)
manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version) manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version)
manifests << manifest manifests << manifest
else
fail!("Failed to retrieve manifest: games:#{app_id}:manifest.xml-#{manifest.base_version}")
return []
end
end end
else
fail!("Failed to retrieve manifest: games:#{app_id}:manifest.xml-#{@target_version}")
return []
end end
@manifests @manifests
end end
def build_package_list(manifests) def build_package_list
@status.operations.clear @status.operations.clear
@status.label = "Downloading #{@application.name}..." @status.label = "Downloading #{@application.name}..."
@status.value = "Building package list..." @status.value = "Building package list..."
@@ -224,44 +264,48 @@ class W3DHub
@status.step = :build_package_list @status.step = :build_package_list
packages = [] # Process manifest game files in OLDEST to NEWEST order so we can simply remove preceeding files from the array that aren't needed
@manifests.reverse.each do |manifest|
manifests.reverse.each do |manifest|
logger.info(LOG_TAG) { "#{manifest.game}-#{manifest.type}: #{manifest.version} (#{manifest.base_version})" } logger.info(LOG_TAG) { "#{manifest.game}-#{manifest.type}: #{manifest.version} (#{manifest.base_version})" }
manifest.files.each do |file| manifest.files.each do |file|
@files["#{file.name}:#{manifest.version}"] = file if file.removed? # No package data
@files.delete_if { |f| f.name.casecmp?(file.name) }
next if file.removed? # No package data @deleted_files.push(file)
next
# if file.patch?
# fail!("#{@application.name} requires patches. Patching is not yet supported.")
# break
# end
next if packages.detect do |pkg|
pkg.category == "games" &&
pkg.subcategory == @app_id &&
pkg.name == file.package &&
pkg.version == manifest.version
end end
packages.push( @files.delete_if { |f| f.name.casecmp?(file.name) } unless file.patch?
Api::Package.new(
{ category: "games", subcategory: @app_id, name: file.package, version: manifest.version }
)
)
packages.last.is_patch = file if file.patch? # If file has been recreated in a newer patch, don't delete it;
# A full file package will exist for it so it will get completely replaced.
@deleted_files.delete_if { |f| f.name.casecmp?(file.name) }
@files.push(file)
end end
# TODO: Dependencies # TODO: Dependencies
end end
packages @files.each do |file|
next if packages.detect do |pkg|
pkg.category == "games" &&
pkg.subcategory == @app_id &&
pkg.name.to_s.casecmp?(file.package.to_s) &&
pkg.version == file.version
end
package = Api::Package.new({ category: "games", subcategory: @app_id, name: file.package, version: file.version })
package.is_patch = file if file.patch?
packages.push(package)
end
@packages = packages
end end
def verify_files(manifests, packages) def verify_files
@status.operations.clear @status.operations.clear
@status.label = "Downloading #{@application.name}..." @status.label = "Downloading #{@application.name}..."
@status.value = "Verifying installed files..." @status.value = "Verifying installed files..."
@@ -278,46 +322,42 @@ class W3DHub
folder_exists = File.directory?(path) folder_exists = File.directory?(path)
manifests.each do |manifest| # Process manifest game files in NEWEST to OLDEST order so that we don't erroneously flag
# valid files as invalid due to an OLDER version of the file being checked FIRST.
@files.reverse.each do |file|
break unless folder_exists break unless folder_exists
manifest.files.each do |file| file_path = normalize_path(file.name, path)
safe_file_name = file.name.gsub("\\", "/")
# Fix borked Data -> data 'cause Windows don't care about capitalization
safe_file_name.sub!("Data/", "data/")
file_path = "#{path}/#{safe_file_name}" processed_files += 1
@status.progress = processed_files.to_f / file_count
processed_files += 1 next if file.removed_since
@status.progress = processed_files.to_f / file_count next if accepted_files.key?(file_path)
next if file.removed_since unless File.exist?(file_path)
next if accepted_files.key?(safe_file_name) rejected_files << { file: file, manifest_version: file.version }
logger.info(LOG_TAG) { "[#{file.version}] File missing: #{file_path}" }
next
end
unless File.exist?(file_path) digest = Digest::SHA256.new
rejected_files << { file: file, manifest_version: manifest.version } f = File.open(file_path)
logger.info(LOG_TAG) { "[#{manifest.version}] File missing: #{file_path}" }
next
end
digest = Digest::SHA256.new while (chunk = f.read(32_000_000))
f = File.open(file_path) digest.update(chunk)
end
while (chunk = f.read(32_000_000)) f.close
digest.update(chunk)
end
f.close logger.info(LOG_TAG) { file.inspect } if file.checksum.nil?
logger.info(LOG_TAG) { file.inspect } if file.checksum.nil? if digest.hexdigest.upcase == file.checksum.upcase
accepted_files[file_path] = file.version
if digest.hexdigest.upcase == file.checksum.upcase logger.info(LOG_TAG) { "[#{file.version}] Verified file: #{file_path}" }
accepted_files[safe_file_name] = manifest.version else
logger.info(LOG_TAG) { "[#{manifest.version}] Verified file: #{file_path}" } rejected_files << { file: file, manifest_version: file.version }
else logger.info(LOG_TAG) { "[#{file.version}] File failed Verification: #{file_path}" }
rejected_files << { file: file, manifest_version: manifest.version }
logger.info(LOG_TAG) { "[#{manifest.version}] File failed Verification: #{file_path}" }
end
end end
end end
@@ -329,7 +369,7 @@ class W3DHub
rejected_files.each do |hash| rejected_files.each do |hash|
next if selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] next if selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"]
package = packages.find { |pkg| pkg.name == hash[:file].package && pkg.version == hash[:manifest_version] } package = @packages.find { |pkg| pkg.name.casecmp?(hash[:file].package) && pkg.version == hash[:manifest_version] }
if package if package
selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] = true selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] = true
@@ -340,13 +380,15 @@ class W3DHub
end end
# Removed packages that don't need to be fetched or processed # Removed packages that don't need to be fetched or processed
packages.delete_if { |package| !selected_packages.find { |pkg| pkg == package } } if folder_exists @packages.delete_if { |package| !selected_packages.find { |pkg| pkg == package } } if folder_exists
packages @packages
end end
def fetch_packages(packages) def fetch_packages
hashes = packages.map do |pkg| return if @packages.empty?
hashes = @packages.map do |pkg|
{ {
category: pkg.category, category: pkg.category,
subcategory: pkg.subcategory, subcategory: pkg.subcategory,
@@ -355,98 +397,103 @@ class W3DHub
} }
end end
package_details = Api.package_details(hashes) package_details = Api.package_details(hashes, @channel.source || :w3dhub)
if package_details unless package_details
@packages = [package_details].flatten
@packages.each do |rich|
package = packages.find do |pkg|
pkg.category == rich.category &&
pkg.subcategory == rich.subcategory &&
"#{pkg.name}.zip" == rich.name &&
pkg.version == rich.version
end
package.instance_variable_set(:"@name", rich.name)
package.instance_variable_set(:"@size", rich.size)
package.instance_variable_set(:"@checksum", rich.checksum)
package.instance_variable_set(:"@checksum_chunk_size", rich.checksum_chunk_size)
package.instance_variable_set(:"@checksum_chunks", rich.checksum_chunks)
end
@packages_to_download = []
@status.label = "Downloading #{@application.name}..."
@status.value = "Verifying local packages..."
@status.progress = 0.0
package_details.each do |pkg|
@status.operations[:"#{pkg.checksum}"] = Status::Operation.new(
label: pkg.name,
value: "Pending...",
progress: 0.0
)
end
@status.step = :prefetch_verifying_packages
package_details.each_with_index.each do |pkg, i|
operation = @status.operations[:"#{pkg.checksum}"]
if verify_package(pkg)
operation.value = "Verified"
operation.progress = 1.0
else
@packages_to_download << pkg
operation.value = "#{W3DHub.format_size(pkg.custom_partially_valid_at_bytes)} / #{W3DHub.format_size(pkg.size)}"
operation.progress = pkg.custom_partially_valid_at_bytes.to_f / pkg.size
end
@status.progress = i.to_f / package_details.count
update_interface_task_status
end
@status.operations.delete_if { |key, o| o.progress >= 1.0 }
@status.step = :fetch_packages
@total_bytes_to_download = @packages_to_download.sum { |pkg| pkg.size - pkg.custom_partially_valid_at_bytes }
@bytes_downloaded = 0
pool = Pool.new(workers: Store.settings[:parallel_downloads])
@packages_to_download.each do |pkg|
pool.add_job Pool::Job.new( proc {
package_bytes_downloaded = pkg.custom_partially_valid_at_bytes
package_fetch(pkg) do |chunk, remaining_bytes, total_bytes|
@bytes_downloaded += chunk.to_s.length
package_bytes_downloaded += chunk.to_s.length
@status.value = "#{W3DHub.format_size(@bytes_downloaded)} / #{W3DHub.format_size(@total_bytes_to_download)}"
@status.progress = @bytes_downloaded.to_f / @total_bytes_to_download
operation = @status.operations[:"#{pkg.checksum}"]
operation.value = "#{W3DHub.format_size(package_bytes_downloaded)} / #{W3DHub.format_size(pkg.size)}"
operation.progress = package_bytes_downloaded.to_f / pkg.size # total_bytes
update_interface_task_status
end
})
end
pool.manage_pool
else
fail!("Failed to fetch package details") fail!("Failed to fetch package details")
return
end end
[package_details].flatten.each do |rich|
if rich.error?
fail!("Failed to retrieve package details! (#{rich.category}:#{rich.subcategory}:#{rich.name}:#{rich.version})\nError: #{rich.error.gsub("-", " ").capitalize}")
return
end
package = @packages.find do |pkg|
pkg.category.to_s.casecmp?(rich.category.to_s) &&
pkg.subcategory.to_s.casecmp?(rich.subcategory.to_s) &&
"#{pkg.name}.zip".casecmp?(rich.name) &&
pkg.version == rich.version
end
package.instance_variable_set(:"@name", rich.name)
package.instance_variable_set(:"@size", rich.size)
package.instance_variable_set(:"@checksum", rich.checksum)
package.instance_variable_set(:"@checksum_chunk_size", rich.checksum_chunk_size)
package.instance_variable_set(:"@checksum_chunks", rich.checksum_chunks)
end
@packages_to_download = []
@status.label = "Downloading #{@application.name}..."
@status.value = "Verifying local packages..."
@status.progress = 0.0
package_details.each do |pkg|
@status.operations[:"#{pkg.checksum}"] = Status::Operation.new(
label: pkg.name,
value: "Pending...",
progress: 0.0
)
end
@status.step = :prefetch_verifying_packages
package_details.each_with_index.each do |pkg, i|
operation = @status.operations[:"#{pkg.checksum}"]
if verify_package(pkg)
operation.value = "Verified"
operation.progress = 1.0
else
@packages_to_download << pkg
operation.value = "#{W3DHub.format_size(pkg.custom_partially_valid_at_bytes)} / #{W3DHub.format_size(pkg.size)}"
operation.progress = pkg.custom_partially_valid_at_bytes.to_f / pkg.size
end
@status.progress = i.to_f / package_details.count
update_interface_task_status
end
@status.operations.delete_if { |key, o| o.progress >= 1.0 }
@status.step = :fetch_packages
@total_bytes_to_download = @packages_to_download.sum { |pkg| pkg.size - pkg.custom_partially_valid_at_bytes }
@bytes_downloaded = 0
pool = Pool.new(workers: Store.settings[:parallel_downloads])
@packages_to_download.each do |pkg|
pool.add_job Pool::Job.new( proc {
package_bytes_downloaded = pkg.custom_partially_valid_at_bytes
package_fetch(pkg) do |chunk, remaining_bytes, total_bytes|
@bytes_downloaded += chunk.to_s.length
package_bytes_downloaded += chunk.to_s.length
@status.value = "#{W3DHub.format_size(@bytes_downloaded)} / #{W3DHub.format_size(@total_bytes_to_download)}"
@status.progress = @bytes_downloaded.to_f / @total_bytes_to_download
operation = @status.operations[:"#{pkg.checksum}"]
operation.value = "#{W3DHub.format_size(package_bytes_downloaded)} / #{W3DHub.format_size(pkg.size)}"
operation.progress = package_bytes_downloaded.to_f / pkg.size # total_bytes
update_interface_task_status
end
})
end
pool.manage_pool
end end
def verify_packages(packages) def verify_packages
end end
def unpack_packages(packages) def unpack_packages
path = Cache.install_path(@application, @channel) path = Cache.install_path(@application, @channel)
logger.info(LOG_TAG) { "Unpacking packages in '#{path}'..." } logger.info(LOG_TAG) { "Unpacking packages in '#{path}'..." }
Cache.create_directories(path, true) Cache.create_directories(path, true)
@@ -456,7 +503,7 @@ class W3DHub
@status.value = "Unpacking..." @status.value = "Unpacking..."
@status.progress = 0.0 @status.progress = 0.0
packages.each do |pkg| @packages.each do |pkg|
# FIXME: can't add a new key into hash during iteration (RuntimeError) # FIXME: can't add a new key into hash during iteration (RuntimeError)
@status.operations[:"#{pkg.checksum}"] = Status::Operation.new( @status.operations[:"#{pkg.checksum}"] = Status::Operation.new(
label: pkg.name, label: pkg.name,
@@ -468,7 +515,7 @@ class W3DHub
@status.step = :unpacking @status.step = :unpacking
i = -1 i = -1
packages.each do |package| @packages.each do |package|
i += 1 i += 1
status = if package.custom_is_patch status = if package.custom_is_patch
@@ -501,6 +548,23 @@ class W3DHub
end end
end end
def remove_deleted_files
return unless @deleted_files.size.positive?
logger.info(LOG_TAG) { "Removing dead files..." }
@deleted_files.each do |file|
logger.info(LOG_TAG) { " #{file.name}" }
path = Cache.install_path(@application, @channel)
file_path = normalize_path(file.name, path)
File.delete(file_path) if File.exist?(file_path)
logger.info(LOG_TAG) { " removed." }
end
end
def create_wine_prefix def create_wine_prefix
if W3DHub.unix? && @wine_prefix if W3DHub.unix? && @wine_prefix
# TODO: create a wine prefix if configured # TODO: create a wine prefix if configured
@@ -513,7 +577,7 @@ class W3DHub
end end
end end
def install_dependencies(packages) def install_dependencies
# TODO: install dependencies # TODO: install dependencies
@status.operations.clear @status.operations.clear
@status.label = "Installing #{@application.name}..." @status.label = "Installing #{@application.name}..."
@@ -523,6 +587,22 @@ class W3DHub
@status.step = :install_dependencies @status.step = :install_dependencies
end end
def write_paths_ini
path = Cache.install_path(@application, @channel)
File.open(normalize_path("data/paths.ini", path), "w") do |file|
file.puts("[paths]")
file.puts("RegBase=W3D Hub")
file.puts("RegClient=#{@application.category}\\#{@application.id}-#{@channel.id}")
file.puts("RegFDS=#{@application.category}\\#{@application.id}-#{@channel.id}-server")
file.puts("FileBase=W3D Hub");
file.puts("FileClient=#{@application.category}\\#{@application.id}-#{@channel.id}")
file.puts("FileFDS=#{@application.category}\\#{@application.id}-#{@channel.id}-server")
file.puts("UseRenFolder=#{@application.uses_ren_folder?}")
end
end
def mark_application_installed def mark_application_installed
Store.application_manager.installed!(self) Store.application_manager.installed!(self)
@@ -544,12 +624,17 @@ class W3DHub
# Check for and integrity of local manifest # Check for and integrity of local manifest
package = nil package = nil
array = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }]) array = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }], @channel.source || :w3dhub)
if array.is_a?(Array) if array.is_a?(Array)
package = array.first package = array.first
else else
fail!("Failed to fetch manifest package details!") fail!("Failed to fetch manifest package details! (#{category}:#{subcategory}:#{name}:#{version})")
return return false
end
if package.error?
fail!("Failed to retrieve manifest package details! (#{category}:#{subcategory}:#{name}:#{version})\nError: #{package.error.gsub("-", " ").capitalize}")
return false
end end
if File.exist?(Cache.package_path(category, subcategory, name, version)) if File.exist?(Cache.package_path(category, subcategory, name, version))
@@ -567,9 +652,13 @@ class W3DHub
def package_fetch(package, &block) def package_fetch(package, &block)
logger.info(LOG_TAG) { "Downloading: #{package.category}:#{package.subcategory}:#{package.name}-#{package.version}" } logger.info(LOG_TAG) { "Downloading: #{package.category}:#{package.subcategory}:#{package.name}-#{package.version}" }
Api.package(package) do |chunk, remaining_bytes, total_bytes| status_okay = Api.package(package) do |chunk, remaining_bytes, total_bytes|
block&.call(chunk, remaining_bytes, total_bytes) block&.call(chunk, remaining_bytes, total_bytes)
end end
fail!("Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})") unless status_okay
status_okay
end end
def verify_package(package, &block) def verify_package(package, &block)
@@ -581,7 +670,7 @@ class W3DHub
return false unless File.exist?(path) return false unless File.exist?(path)
operation = @status.operations[:"#{package.checksum}"] operation = @status.operations[:"#{package.checksum}"]
operation&.value = "Verifying..." operation&.value = "Verifying..."
file_size = File.size(path) file_size = File.size(path)
logger.info(LOG_TAG) { " File size: #{file_size}" } logger.info(LOG_TAG) { " File size: #{file_size}" }
@@ -646,28 +735,28 @@ class W3DHub
logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" } logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" }
unzip(package_path, temp_path) unzip(package_path, temp_path)
# Fix borked Data -> data 'cause Windows don't care about capitalization file_path = normalize_path(manifest_file.name, path)
safe_file_name = "#{manifest_file.name.sub('Data/', 'data/')}" temp_file_path = normalize_path(manifest_file.name, temp_path)
logger.info(LOG_TAG) { " Loading #{temp_path}/#{safe_file_name}.patch..." } logger.info(LOG_TAG) { " Loading #{temp_file_path}.patch..." }
patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_path}/#{safe_file_name}.patch", ignore_crc_mismatches: false) patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_file_path}.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) patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name.casecmp?(".w3dhub.patch") || f.name.casecmp?(".bhppatch") }.data, symbolize_names: true)
logger.info(LOG_TAG) { " Loading #{path}/#{safe_file_name}..." } logger.info(LOG_TAG) { " Loading #{file_path}..." }
target_mix = W3DHub::Mixer::Reader.new(file_path: "#{path}/#{safe_file_name}", ignore_crc_mismatches: false) target_mix = W3DHub::Mixer::Reader.new(file_path: "#{file_path}", ignore_crc_mismatches: false)
logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive? logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive?
patch_info[:removedFiles].each do |file| patch_info[:removedFiles].each do |file|
logger.debug(LOG_TAG) { " #{file}" } logger.debug(LOG_TAG) { " #{file}" }
target_mix.package.files.delete_if { |f| f.name == file } target_mix.package.files.delete_if { |f| f.name.casecmp?(file) }
end end
logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive? logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive?
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.package.files.find { |f| f.name == file } patch = patch_mix.package.files.find { |f| f.name.casecmp?(file) }
target = target_mix.package.files.find { |f| f.name == file } target = target_mix.package.files.find { |f| f.name.casecmp?(file) }
if target if target
target_mix.package.files[target_mix.package.files.index(target)] = patch target_mix.package.files[target_mix.package.files.index(target)] = patch
@@ -676,8 +765,8 @@ class W3DHub
end end
end end
logger.info(LOG_TAG) { " Writing updated #{path}/#{safe_file_name}..." } if patch_info[:updatedFiles].size.positive? logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
W3DHub::Mixer::Writer.new(file_path: "#{path}/#{safe_file_name}", package: target_mix.package, memory_buffer: true) W3DHub::Mixer::Writer.new(file_path: "#{file_path}", package: target_mix.package, memory_buffer: true, encrypted: target_mix.encrypted?)
FileUtils.remove_dir(temp_path) FileUtils.remove_dir(temp_path)
@@ -688,17 +777,15 @@ class W3DHub
stream = Zip::InputStream.new(File.open(package_path)) stream = Zip::InputStream.new(File.open(package_path))
while (entry = stream.get_next_entry) while (entry = stream.get_next_entry)
# Normalize the path to handle case-insensitivity consistently
file_path = normalize_path(entry.name, path)
safe_file_name = entry.name.gsub("\\", "/") dir_path = File.dirname(file_path)
# Fix borked Data -> data 'cause Windows don't care about capitalization
safe_file_name.sub!("Data/", "data/")
dir_path = "#{path}/#{File.dirname(safe_file_name)}"
unless dir_path.end_with?("/.") || Dir.exist?(dir_path) unless dir_path.end_with?("/.") || Dir.exist?(dir_path)
FileUtils.mkdir_p(dir_path) FileUtils.mkdir_p(dir_path)
end end
File.open("#{path}/#{safe_file_name}", "wb") do |f| File.open(file_path, "wb") do |f|
i = entry.get_input_stream i = entry.get_input_stream
while (chunk = i.read(32_000_000)) # Read up to ~32 MB per chunk while (chunk = i.read(32_000_000)) # Read up to ~32 MB per chunk

View File

@@ -1,26 +0,0 @@
class W3DHub
class ApplicationManager
class Importer < Task
LOG_TAG = "W3DHub::ApplicationManager::Importer".freeze
def type
:importer
end
def execute_task
path = W3DHub.ask_file
unless File.exist?(path) && !File.directory?(path)
fail!("File #{path.inspect} does not exist or is a directory")
fail_silently! if path.nil? || path&.length&.zero? # User likely canceled the file selection
end
return false if failed?
Store.application_manager.imported!(self, path)
true
end
end
end
end

View File

@@ -13,28 +13,34 @@ class W3DHub
fail_fast fail_fast
return false if failed? return false if failed?
manifests = fetch_manifests fetch_manifests
return false if failed? return false if failed?
packages = build_package_list(manifests) build_package_list
return false if failed? return false if failed?
verify_files(manifests, packages) remove_deleted_files
return false if failed? return false if failed?
fetch_packages(packages) verify_files
return false if failed? return false if failed?
verify_packages(packages) fetch_packages
return false if failed? return false if failed?
unpack_packages(packages) verify_packages
return false if failed?
unpack_packages
return false if failed? return false if failed?
create_wine_prefix create_wine_prefix
return false if failed? return false if failed?
install_dependencies(packages) install_dependencies
return false if failed?
write_paths_ini
return false if failed? return false if failed?
mark_application_installed mark_application_installed

View File

@@ -6,24 +6,6 @@ class W3DHub
def type def type
:repairer :repairer
end end
# def execute_task
# fail_fast
# return false if failed?
# manifests = fetch_manifests
# return false if failed?
# packages = build_package_list(manifests)
# return false if failed?
# verify_files(manifests, packages)
# return false if failed?
# # pp packages.select { |pkg| pkg.name == "misc" }
# true
# end
end end
end end
end end

View File

@@ -23,7 +23,12 @@ class W3DHub
def self.verify_peer def self.verify_peer
no_verify.tap do |context| no_verify.tap do |context|
context.verify_mode = OpenSSL::SSL::VERIFY_PEER context.verify_mode = OpenSSL::SSL::VERIFY_PEER
context.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths) context.cert_store = OpenSSL::X509::Store.new
if (ca_file = W3DHub.ca_bundle_path)
context.cert_store.add_file(ca_file)
else
context.cert_store.set_default_paths
end
end end
end end

View File

@@ -7,31 +7,31 @@ class W3DHub
theme W3DHub::THEME theme W3DHub::THEME
background 0xaa_444444 background 0x88_525252
stack(width: 1.0, max_width: 760, height: 1.0, max_height: 256, v_align: :center, h_align: :center, background: 0xff_222222) do stack(width: 1.0, max_width: 760, height: 1.0, max_height: 268, v_align: :center, h_align: :center, background: 0xee_222222) do
# Title bar # Title bar
flow(width: 1.0, height: 32, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000 background 0x88_000000
# image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff # image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff
# tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center # tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center
tagline @game ? "Update Game" : "Add Game", width: 1.0, text_align: :center title @game ? "Update Game" : "Add Game", width: 1.0, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "Game or Mod Title:" para "Game or Mod Title:"
@game_title = edit_line "#{@game&.title}", width: 1.0, fill: true @game_title = edit_line "#{@game&.title}", width: 1.0, fill: true
end end
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "Path to Executable:" para "Path to Executable:"
flow(width: 1.0, fill: true) do flow(width: 1.0, fill: true) do
@game_path = edit_line "#{@game&.path}", fill: true, height: 1.0 @game_path = edit_line "#{@game&.path}", fill: true, height: 1.0
button "Browse...", width: 128, height: 1.0, enabled: W3DHub.unix?, tip: W3DHub.unix? ? "Browse for game executable" : "Not available on Windows" do button "Browse...", width: 128, height: 1.0, tip: "Browse for game executable" do
path = W3DHub.ask_file path = W3DHub.ask_file
@game_path.value = path if !path.empty? && File.exist?(path) @game_path.value = path if !path.empty? && File.exist?(path)
end end
@@ -40,7 +40,7 @@ class W3DHub
flow(fill: true) flow(fill: true)
flow(width: 1.0, margin_top: 8, height: 40, padding_bottom: 8) do flow(width: 1.0, margin_top: 8, height: 46, padding_bottom: 8) do
button "Cancel", fill: true, margin_right: 4 do button "Cancel", fill: true, margin_right: 4 do
pop_state pop_state
end end

View File

@@ -7,24 +7,24 @@ class W3DHub
theme W3DHub::THEME theme W3DHub::THEME
background 0xaa_444444 background 0x88_525252
stack(width: 1.0, max_width: 760, height: 1.0, max_height: 560, v_align: :center, h_align: :center, background: 0xff_222222) do stack(width: 1.0, max_width: 760, height: 1.0, max_height: 610, v_align: :center, h_align: :center, background: 0xee_222222) do
# Title bar # Title bar
flow(width: 1.0, height: 32, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000 background 0x88_000000
# tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center # tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center
tagline @profile ? "Update IRC Profile" : "Add IRC Profile", width: 1.0, fill: true, text_align: :center title @profile ? "Update IRC Profile" : "Add IRC Profile", width: 1.0, fill: true, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "IRC Nickname:" para "IRC Nickname:"
@nickname = edit_line "#{@profile&.nickname}", width: 1.0, fill: true @nickname = edit_line "#{@profile&.nickname}", width: 1.0, fill: true
end end
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
flow(width: 1.0, height: 1.0) do flow(width: 1.0, height: 1.0) do
stack(width: 0.5, height: 1.0) do stack(width: 0.5, height: 1.0) do
para "IRC Username (Optional):" para "IRC Username (Optional):"
@@ -38,7 +38,7 @@ class W3DHub
end end
end end
stack(width: 1.0, height: 66, margin_top: 32) do stack(width: 1.0, height: 72, margin_top: 32) do
flow(width: 1.0, height: 1.0) do flow(width: 1.0, height: 1.0) do
stack(width: 0.75, height: 1.0) do stack(width: 0.75, height: 1.0) do
para "IRC Server IP or Hostname:" para "IRC Server IP or Hostname:"
@@ -52,23 +52,30 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 66, margin_top: 8) do flow(width: 1.0, height: 72, margin_top: 8) do
@server_ssl = check_box "IRC Server Use SSL", checked: @profile&.server_ssl, text_size: 18, width: 0.5, height: 66 flow(width: 0.5, height: 1.0) do
@server_verify_ssl = check_box "IRC Verify Server SSL Certificate", checked: @profile ? @profile.server_verify_ssl : true, text_size: 18, width: 0.5, height: 66 @server_ssl = toggle_button checked: @profile&.server_ssl, text_size: 18, height: 18
para "IRC Server Use SSL", fill: true, text_wrap: :none, margin_left: 8
end
flow(width: 0.5, height: 1.0) do
@server_verify_ssl = toggle_button checked: @profile ? @profile.server_verify_ssl : true, text_size: 18, height: 18
para "IRC Verify Server SSL Certificate", fill: true, text_wrap: :none, margin_left: 8
end
end end
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "Brenbot Bot Name:" para "Brenbot Bot Name:"
@bot_username = edit_line "#{@profile&.bot_username}", width: 1.0, fill: true @bot_username = edit_line "#{@profile&.bot_username}", width: 1.0, fill: true
end end
flow(width: 1.0, height: 66) do flow(width: 1.0, height: 72) do
stack(width: 0.5, height: 66) do stack(width: 0.5, height: 72) do
para "Brenbot Auth Username:" para "Brenbot Auth Username:"
@bot_auth_username = edit_line "#{@profile&.bot_auth_username}", width: 1.0, fill: true @bot_auth_username = edit_line "#{@profile&.bot_auth_username}", width: 1.0, fill: true
end end
stack(width: 0.5, height: 66) do stack(width: 0.5, height: 72) do
para "Brenbot Auth Password:" para "Brenbot Auth Password:"
@bot_auth_password = edit_line @profile && @profile.bot_auth_password ? Base64.strict_decode64(@profile.bot_auth_password) : "", width: 1.0, fill: true, type: :password @bot_auth_password = edit_line @profile && @profile.bot_auth_password ? Base64.strict_decode64(@profile.bot_auth_password) : "", width: 1.0, fill: true, type: :password
end end
@@ -76,7 +83,7 @@ class W3DHub
flow(fill: true) flow(fill: true)
flow(width: 1.0, margin_top: 8, height: 40, padding_bottom: 8) do flow(width: 1.0, margin_top: 8, height: 46, padding_bottom: 8) do
button "Cancel", fill: true, margin_right: 4 do button "Cancel", fill: true, margin_right: 4 do
pop_state pop_state
end end

View File

@@ -7,21 +7,21 @@ class W3DHub
theme W3DHub::THEME theme W3DHub::THEME
background 0xaa_444444 background 0x88_525252
stack(width: 1.0, max_width: 760, height: 1.0, max_height: 256, v_align: :center, h_align: :center, background: 0xff_222222) do stack(width: 1.0, max_width: 760, height: 1.0, max_height: 272, v_align: :center, h_align: :center, background: 0xee_222222) do
# Title bar # Title bar
flow(width: 1.0, height: 32, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000 background 0x88_000000
# image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff # image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff
# tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center # tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center
tagline @server_profile ? "Update Server Profile" : "Add Server Profile", width: 1.0, text_align: :center title @server_profile ? "Update Server Profile" : "Add Server Profile", width: 1.0, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "Server Profile Name:" para "Server Profile Name:"
@server_name = edit_line "#{@server_profile&.name}", width: 1.0, fill: true @server_name = edit_line "#{@server_profile&.name}", width: 1.0, fill: true
@server_name.subscribe(:changed) do |label| @server_name.subscribe(:changed) do |label|
@@ -31,7 +31,7 @@ class W3DHub
flow(fill: true) flow(fill: true)
flow(width: 1.0, height: 40, padding_bottom: 8) do flow(width: 1.0, height: 46, padding_bottom: 8) do
button "Cancel", fill: true, margin_right: 4 do button "Cancel", fill: true, margin_right: 4 do
pop_state pop_state
end end

View File

@@ -43,20 +43,20 @@ class W3DHub
@@instance.kill! @@instance.kill!
end end
def self.job(job, callback, error_handler = nil) def self.job(job, callback, error_handler = nil, data = nil)
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler)) @@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, data: data))
end end
def self.parallel_job(job, callback, error_handler = nil) def self.parallel_job(job, callback, error_handler = nil, data = nil)
@@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler)) @@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, data: data))
end end
def self.foreground_job(job, callback, error_handler = nil) def self.foreground_job(job, callback, error_handler = nil, data = nil)
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true)) @@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true, data: data))
end end
def self.foreground_parallel_job(job, callback, error_handler = nil) def self.foreground_parallel_job(job, callback, error_handler = nil, data = nil)
@@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true)) @@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true, data: data))
end end
def initialize def initialize
@@ -136,16 +136,16 @@ class W3DHub
end end
class Job class Job
def initialize(job:, callback:, error_handler: nil, deliver_to_queue: false) def initialize(job:, callback:, error_handler: nil, deliver_to_queue: false, data: nil)
@job = job @job = job
@callback = callback @callback = callback
@error_handler = error_handler @error_handler = error_handler
@deliver_to_queue = deliver_to_queue @deliver_to_queue = deliver_to_queue
@data = data
end end
def do def do
result = @job.call result = @data ? @job.call(@data) : @job.call
deliver(result) deliver(result)
end end

View File

@@ -9,18 +9,18 @@ class W3DHub
end end
# Fetch a generic uri # 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) path = path(uri)
if !force_fetch && File.exist?(path) if !force_fetch && File.exist?(path)
path path
elsif async elsif async
BackgroundWorker.job( 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 } ->(response) { File.open(path, "wb") { |f| f.write response.body } if response.status == 200 }
) )
else 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 File.open(path, "wb") { |f| f.write response.body } if response.status == 200
end end
end end
@@ -69,7 +69,7 @@ class W3DHub
body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}" 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 total_bytes = package.size
remaining_bytes = total_bytes - start_from_bytes remaining_bytes = total_bytes - start_from_bytes
@@ -89,9 +89,15 @@ class W3DHub
# Download a W3D Hub package # Download a W3D Hub package
def self.fetch_package(package, block) def self.fetch_package(package, block)
endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package"
if package.download_url
uri_path = package.download_url.split("/").last
endpoint_download_url = package.download_url.sub(uri_path, URI.encode_uri_component(uri_path))
end
path = package_path(package.category, package.subcategory, package.name, package.version) 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 = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT }
headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account && !package.download_url
body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}"
start_from_bytes = package.custom_partially_valid_at_bytes start_from_bytes = package.custom_partially_valid_at_bytes
logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" } logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" }
@@ -112,16 +118,39 @@ class W3DHub
end end
# Create a new connection due to some weirdness somewhere in Excon # Create a new connection due to some weirdness somewhere in Excon
response = Excon.post( response = Excon.send(
"#{Api::ENDPOINT}/apis/launcher/1/get-package", package.download_url ? :get : :post,
endpoint_download_url,
tcp_nodelay: true, tcp_nodelay: true,
headers: headers, headers: headers,
body: "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}", body: package.download_url ? "" : body,
chunk_size: 50_000, chunk_size: 50_000,
response_block: streamer response_block: streamer,
middlewares: Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower]
) )
response.status == 200 || response.status == 206 if response.status == 200 || response.status == 206
return true
else
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false
end
rescue Excon::Error::Timeout => e
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" timed out after: #{W3DHub::Api::API_TIMEOUT} seconds" }
logger.error(LOG_TAG) { e }
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false
rescue Excon::Error => e
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" errored:" }
logger.error(LOG_TAG) { e }
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false
ensure ensure
file&.close file&.close
end end

View File

@@ -32,6 +32,15 @@ class W3DHub
linux? || mac? linux? || mac?
end end
# Detect system CA bundle path for SSL verification
def self.ca_bundle_path
[
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
"/etc/pki/tls/certs/ca-bundle.crt", # RHEL/Fedora/CentOS
"/etc/ssl/ca-bundle.pem" # Some other distros
].find { |path| File.exist?(path) }
end
def self.url(path) def self.url(path)
raise "Hazardous input: #{path}" if path.include?("&&") || path.include?(";") raise "Hazardous input: #{path}" if path.include?("&&") || path.include?(";")
@@ -72,15 +81,19 @@ class W3DHub
) )
end end
def self.join_server(server, password) 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) &&
Store.settings[:server_list_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, password server.channel,
server,
username,
password,
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: "?")
@@ -166,7 +179,37 @@ class W3DHub
path.strip path.strip
else else
raise NotImplementedError result_ptr = LibUI.open_file(LIBUI_WINDOW)
result = result_ptr.null? ? "" : result_ptr.to_s.gsub("\\", "/")
result.strip
end
end
def self.ask_folder(title: "Open Folder")
if W3DHub.unix?
# search for command
cmds = %w{ zenity matedialog qarma kdialog }
command = cmds.find do |cmd|
cmd if system("which #{cmd}")
end
path = case File.basename(command)
when "zenity", "matedialog", "qarma"
`#{command} --file-selection --directory --title "#{title}"`
when "kdialog"
`#{command} --title "#{title}" --getexistingdirectory #{Dir.home}"`
else
raise "No known command found for system file selection dialog!"
end
path.strip
else
result_ptr = LibUI.open_folder(LIBUI_WINDOW)
result = result_ptr.null? ? "" : result_ptr.to_s.gsub("\\", "/")
result.strip
end end
end end
end end

View File

@@ -1,52 +0,0 @@
module CyberarmEngine
class GuiState < CyberarmEngine::GameState
def menu(host_element, items:, width: 200)
container = CyberarmEngine::Element::Stack.new(
parent: host_element.parent,
width: width,
theme: W3DHub::THEME,
border_color: 0xff_000000,
border_thickness: 1
)
container.instance_variable_set(:"@__menu", host_element)
container.define_singleton_method(:recalculate_menu) do
@x = @__menu.x
@y = @__menu.y + @__menu.height
@y = @__menu.y - height if @y + height > window.height
end
def container.recalculate
super
recalculate_menu
end
items.each do |item|
btn = CyberarmEngine::Element::Button.new(
item[:label],
{
parent: container,
width: 1.0,
text_align: :left,
theme: W3DHub::THEME,
border_thickness: 0,
margin: 0
},
proc do
item[:block]&.call
end
)
container.add(btn)
end
container.recalculate
container.recalculate
container.recalculate
show_menu(container)
end
end
end

View File

@@ -6,6 +6,8 @@ class W3DHub
# https://github.com/TheUnstoppable/MixLibrary used for reference # https://github.com/TheUnstoppable/MixLibrary used for reference
class Mixer class Mixer
DEFAULT_BUFFER_SIZE = 32_000_000 DEFAULT_BUFFER_SIZE = 32_000_000
MIX1_HEADER = 0x3158494D
MIX2_HEADER = 0x3258494D
class MixParserException < RuntimeError; end class MixParserException < RuntimeError; end
class MixFormatException < RuntimeError; end class MixFormatException < RuntimeError; end
@@ -203,8 +205,12 @@ class W3DHub
@buffer.pos = 0 @buffer.pos = 0
@encrypted = false
# Valid header # Valid header
if read_i32 == 0x3158494D if (mime = read_i32) && (mime == MIX1_HEADER || mime == MIX2_HEADER)
@encrypted = mime == MIX2_HEADER
file_data_offset = read_i32 file_data_offset = read_i32
file_names_offset = read_i32 file_names_offset = read_i32
@@ -237,7 +243,7 @@ class W3DHub
@buffer.pos = pos @buffer.pos = pos
end end
else else
raise MixParserException, "Invalid MIX file" raise MixParserException, "Invalid MIX file: Expected \"#{MIX1_HEADER}\" or \"#{MIX2_HEADER}\", got \"0x#{mime.to_s(16).upcase}\"\n(#{file_path})"
end end
ensure ensure
@@ -264,18 +270,24 @@ class W3DHub
buffer.strip buffer.strip
end end
def encrypted?
@encrypted
end
end end
class Writer class Writer
attr_reader :package attr_reader :package
def initialize(file_path:, package:, memory_buffer: false, buffer_size: DEFAULT_BUFFER_SIZE) def initialize(file_path:, package:, memory_buffer: false, buffer_size: DEFAULT_BUFFER_SIZE, encrypted: false)
@package = package @package = package
@buffer = MemoryBuffer.new(file_path: file_path, mode: :write, buffer_size: buffer_size) @buffer = MemoryBuffer.new(file_path: file_path, mode: :write, buffer_size: buffer_size)
@buffer.pos = 0 @buffer.pos = 0
@buffer.write("MIX1") @encrypted = encrypted
@buffer.write(encrypted? ? "MIX2" : "MIX1")
files = @package.files.sort { |a, b| a.file_crc <=> b.file_crc } files = @package.files.sort { |a, b| a.file_crc <=> b.file_crc }
@@ -322,6 +334,10 @@ class W3DHub
def write_byte(byte) def write_byte(byte)
@buffer.write([byte].pack("c")) @buffer.write([byte].pack("c"))
end end
def encrypted?
@encrypted
end
end end
# Eager loads patch file and streams target file metadata (doen't load target file data or generate CRCs) # Eager loads patch file and streams target file metadata (doen't load target file data or generate CRCs)

View File

@@ -47,9 +47,5 @@ class W3DHub
def button_up(id) def button_up(id)
end end
def menu(host_element, items:)
@host.menu(host_element, items: items)
end
end end
end end

View File

@@ -3,6 +3,7 @@ class W3DHub
class Community < Page class Community < Page
def setup def setup
@w3dhub_news ||= nil @w3dhub_news ||= nil
@w3dhub_news_expires ||= 0
body.clear do body.clear do
stack(width: 1.0, height: 1.0, padding: 8) do stack(width: 1.0, height: 1.0, padding: 8) do
@@ -45,10 +46,10 @@ class W3DHub
tagline "<b>Help & Support</b>" tagline "<b>Help & Support</b>"
flow(width: 1.0) do flow(width: 1.0) do
para "For help and support using this launcher or playing any W3D Hub game visit the" para "For help and support using this launcher or playing any W3D Hub game visit the"
link("W3D Hub forums", text_size: 16, tip: "https://w3dhub.com/forum/") { W3DHub.url("https://w3dhub.com/forum/") } link("W3D Hub forums", text_size: 22, tip: "https://w3dhub.com/forum/") { W3DHub.url("https://w3dhub.com/forum/") }
para "or join us in" para "or join us in"
image "#{GAME_ROOT_PATH}/media/social_media_icons/discord.png", height: 16, padding_top: 4 image "#{GAME_ROOT_PATH}/media/social_media_icons/discord.png", height: 16, padding_top: 4
link("#tech-support", text_size: 16, tip: "https://discord.com/invite/GYhW7eV") { W3DHub.url("https://discord.com/invite/GYhW7eV") } link("#tech-support", text_size: 22, tip: "https://discord.com/invite/GYhW7eV") { W3DHub.url("https://discord.com/invite/GYhW7eV") }
para "on the W3D Hub Discord server" para "on the W3D Hub Discord server"
end end
end end
@@ -76,6 +77,30 @@ class W3DHub
end end
end end
def update
super
if Gosu.milliseconds >= @w3dhub_news_expires
@w3dhub_news = nil
@w3dhub_news_expires = Gosu.milliseconds + 30_000 # seconds
@wd3hub_news_container.clear do
title I18n.t(:"games.fetching_news"), padding: 8
end
BackgroundWorker.foreground_job(
-> { fetch_w3dhub_news },
lambda do |result|
if result
populate_w3dhub_news
Cache.release_net_lock(result)
end
end
)
end
end
def fetch_w3dhub_news def fetch_w3dhub_news
lock = Cache.acquire_net_lock("w3dhub_news") lock = Cache.acquire_net_lock("w3dhub_news")
return false unless lock return false unless lock
@@ -86,10 +111,11 @@ class W3DHub
return unless news return unless news
news.items[0..15].each do |item| news.items[0..15].each do |item|
Cache.fetch(uri: item.image, async: false) Cache.fetch(uri: item.image, async: false, backend: :w3dhub)
end end
@w3dhub_news = news @w3dhub_news = news
@w3dhub_news_expires = Gosu.milliseconds + (60 * 60 * 1000) # 1 hour (in ms)
"w3dhub_news" "w3dhub_news"
end end
@@ -113,15 +139,15 @@ class W3DHub
# stack(width: 0.6, height: 1.0) do # stack(width: 0.6, height: 1.0) do
# stack(width: 1.0, height: 112) do # stack(width: 1.0, height: 112) do
# link "<b>#{item.title}</b>", text_size: 18 do # link "<b>#{item.title}</b>", text_size: 22 do
# W3DHub.url(item.uri) # W3DHub.url(item.uri)
# end # end
# inscription item.blurb.gsub(/\n+/, "\n").strip[0..180] # para item.blurb.gsub(/\n+/, "\n").strip[0..180]
# end # end
# flow(width: 1.0) do # flow(width: 1.0) do
# inscription item.timestamp.strftime("%Y-%m-%d"), width: 0.499 # para item.timestamp.strftime("%Y-%m-%d"), width: 0.499
# link I18n.t(:"games.read_more"), width: 0.5, text_align: :right, text_size: 14 do # link I18n.t(:"games.read_more"), width: 0.5, text_align: :right, text_size: 22 do
# W3DHub.url(item.uri) # W3DHub.url(item.uri)
# end # end
# end # end
@@ -133,21 +159,24 @@ class W3DHub
image_path = Cache.path(item.image) image_path = Cache.path(item.image)
flow(width: 1.0, max_width: 1230, height: 200, margin: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(0xff_252525))) do flow(width: 1.0, max_width: 1230, height: 200, margin: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(0xff_252525))) do
background 0x22_000000 background 0x44_000000
image image_path, height: 1.0 # Ensure the image file exists before trying to load it
if File.exist?(image_path)
image image_path, height: 1.0
else
logger.warn("W3DHub::Community") { "Image not found in cache: #{image_path}" }
image BLACK_IMAGE, height: 1.0
end
stack(fill: true, height: 1.0, background: 0x44_000000, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(0xff_252525))) do stack(fill: true, height: 1.0, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(0xff_252525))) do
tagline "<b>#{item.title}</b>", width: 1.0 tagline "<b>#{item.title}</b>", width: 1.0
inscription item.blurb.gsub(/\n+/, "\n").strip[0..1024], fill: true para item.blurb.gsub(/\n+/, "\n").strip[0..1024], fill: true
flow(fill: true) flow(width: 1.0, height: 36, margin_top: 8) do
flow(width: 1.0, height: 32, margin_top: 8) do
stack(fill: true, height: 1.0) do stack(fill: true, height: 1.0) do
flow(fill: true) flow(fill: true)
inscription "#{item.author}#{item.timestamp.strftime("%Y-%m-%d")}" para "#{item.author}#{item.timestamp.strftime("%Y-%m-%d")}"
flow(fill: true)
end end
button I18n.t(:"games.read_more"), width: 1.0, max_width: 128, padding_top: 4, padding_bottom: 4 do button I18n.t(:"games.read_more"), width: 1.0, max_width: 128, padding_top: 4, padding_bottom: 4 do

View File

@@ -31,27 +31,29 @@ class W3DHub
# TODO: Show correct application details here # TODO: Show correct application details here
flow(width: 1.0, height: 0.1, padding: 8) do flow(width: 1.0, height: 0.1, padding: 8) do
background task.application.color app_color = Gosu::Color.new(task.application.color)
app_color.alpha = 0x88
background app_color
flow(width: 0.70, height: 1.0) do flow(width: 0.70, height: 1.0) do
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{task.app_id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{task.app_id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png" image_path = File.exist?("#{CACHE_PATH}/#{task.app_id}.png") ? "#{CACHE_PATH}/#{task.app_id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
@application_image = image image_path, height: 1.0 @application_image = image image_path, height: 1.0
stack(margin_left: 8, width: 0.75) do stack(margin_left: 8, width: 0.75) do
@application_name_label = tagline "#{task.application.name}" @application_name_label = tagline "#{task.application.name}"
@application_version_label = inscription "Version: #{task.target_version} (#{task.channel.id})" @application_version_label = para "Version: #{task.target_version} (#{task.channel.id})"
end end
end end
flow(width: 0.30, height: 1.0) do flow(width: 0.30, height: 1.0) do
stack(width: 0.499, height: 1.0) do stack(width: 0.499, height: 1.0) do
para "Download Speed", width: 1.0, text_align: :center para "Download Speed", width: 1.0, text_align: :center
@download_speed_label = inscription "- b/s", width: 1.0, text_align: :center @download_speed_label = para "- b/s", width: 1.0, text_align: :center
end end
stack(width: 0.5, height: 1.0) do stack(width: 0.5, height: 1.0) do
para "Downloaded", width: 1.0, text_align: :center para "Downloaded", width: 1.0, text_align: :center
inscription "---- b / ---- b", width: 1.0, text_align: :center para "---- b / ---- b", width: 1.0, text_align: :center
end end
end end
end end
@@ -64,15 +66,15 @@ class W3DHub
task.status.operations.each do |key, operation| task.status.operations.each do |key, operation|
i += 1 i += 1
stack(width: 1.0, height: 24, padding: 8) do stack(width: 1.0, height: 26, padding: 8) do
background 0xff_333333 if i.odd? background 0xaa_333333 if i.odd?
flow(width: 1.0, height: 22) do flow(width: 1.0, height: 22) do
@operation_info["#{key}_name"] = inscription operation.label, width: 0.7, text_wrap: :none, tag: "#{key}_name" @operation_info["#{key}_name"] = para operation.label, width: 0.7, text_wrap: :none, tag: "#{key}_name"
@operation_info["#{key}_status"] = inscription operation.value, width: 0.3, text_align: :right, text_wrap: :none, tag: "#{key}_status" @operation_info["#{key}_status"] = para operation.value, width: 0.3, text_align: :right, text_wrap: :none, tag: "#{key}_status"
end end
@operation_info["#{key}_progress"] = progress fraction: operation.progress, height: 2, width: 1.0, tag: "#{key}_progress" @operation_info["#{key}_progress"] = progress fraction: operation.progress, height: 2, width: 1.0, margin_top: 2, tag: "#{key}_progress"
end end
end end
end end

View File

@@ -17,8 +17,7 @@ class W3DHub
end end
# Game Menu # Game Menu
@game_page_container = stack(width: 1.0, fill: true, background_image: "#{GAME_ROOT_PATH}/media/textures/noiseb.png", background_image_mode: :tiled) do @game_page_container = stack(width: 1.0, fill: true) do
# , background_image: "C:/Users/cyber/Downloads/vlcsnap-2022-04-24-22h24m15s854.png"
end end
end end
end end
@@ -29,6 +28,36 @@ class W3DHub
populate_games_list populate_games_list
end end
def update
super
@game_news.each do |key, value|
next if key.end_with?("_expires")
if Gosu.milliseconds >= @game_news["#{key}_expires"]
@game_news.delete(key)
@game_news["#{key}_expires"] = Gosu.milliseconds + 30_000 # seconds
if @focused_game && @focused_game.id == key
@game_news_container.clear do
title I18n.t(:"games.fetching_news"), padding: 8
end
BackgroundWorker.foreground_job(
-> { fetch_game_news(@focused_game) },
lambda do |result|
if result
populate_game_news(@focused_game)
Cache.release_net_lock(result)
end
end
)
end
end
end
end
def populate_games_list def populate_games_list
@games_list_container.clear do @games_list_container.clear do
background 0xaa_121920 background 0xaa_121920
@@ -51,19 +80,19 @@ class W3DHub
selected = game == @focused_game selected = game == @focused_game
game_button = stack(width: 64, height: 1.0, border_thickness_bottom: 4, game_button = stack(width: 64, height: 1.0, border_thickness_bottom: 4,
border_color_bottom: selected ? 0xff_00acff : 0x00_000000, border_color_bottom: selected ? 0xff_0074e0 : 0x00_000000,
hover: { background: selected ? game.color : 0xff_444444 }, hover: { background: selected ? game.color : 0xff_444444 },
padding_left: 4, padding_right: 4, tip: game.name) do padding_left: 4, padding_right: 4, tip: game.name) do
background game.color if selected background game.color if selected
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{game.id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png" image_path = File.exist?("#{CACHE_PATH}/#{game.id}.png") ? "#{CACHE_PATH}/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
image_color = Store.application_manager.installed?(game.id, game.channels.first.id) ? 0xff_ffffff : 0x66_ffffff image_color = Store.application_manager.installed?(game.id, game.channels.first.id) ? 0xff_ffffff : 0x66_ffffff
flow(width: 1.0, height: 1.0, margin: 8, background_image: image_path, background_image_color: image_color, background_image_mode: :fill_height) do flow(width: 1.0, height: 1.0, margin: 8, background_image: image_path, background_image_color: image_color, background_image_mode: :fill_height) do
image "#{GAME_ROOT_PATH}/media/ui_icons/import.png", width: 24, margin_left: -4, margin_top: -6, color: 0xff_ff8800 if Store.application_manager.updateable?(game.id, game.channels.first.id) image "#{GAME_ROOT_PATH}/media/ui_icons/import.png", width: 24, margin_left: -4, margin_top: -6, color: 0xff_ff8800 if game.channels.any? { |channel| Store.application_manager.updateable?(game.id, channel.id) }
end end
# inscription game.name, width: 1.0, text_align: :center, text_size: 14 # para game.name, width: 1.0, text_align: :center
end end
def game_button.hit_element?(x, y) def game_button.hit_element?(x, y)
@@ -71,7 +100,9 @@ class W3DHub
end end
game_button.subscribe(:clicked_left_mouse_button) do game_button.subscribe(:clicked_left_mouse_button) do
populate_game_page(game, game.channels.first) channel = @focused_game == game ? @focused_channel : game.channels.first
populate_game_page(game, channel)
populate_games_list populate_games_list
end end
end end
@@ -87,28 +118,25 @@ class W3DHub
@game_page_container.clear do @game_page_container.clear do
game_color = Gosu::Color.new(game.color) game_color = Gosu::Color.new(game.color)
game_color.alpha = 0x88 game_color.alpha = 0xaa
background game_color background_image_path = Cache.package_path(game.category, game.id, "background.png", "")
@game_page_container.style.background_image_color = game_color if File.exist?(background_image_path)
@game_page_container.style.default[:background_image_color] = game_color States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path)
@game_page_container.update_background_image States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.default[:background_image] = get_image(background_image_path)
end
# Game Stuff # Game Stuff
flow(width: 1.0, fill: true) do flow(width: 1.0, fill: true) do
# background 0xff_9999ff
# Game options # Game options
stack(width: 360, height: 1.0, padding: 8, scroll: true, border_thickness_right: 1, border_color_right: W3DHub::BORDER_COLOR) do stack(width: 360, height: 1.0, padding: 8, scroll: true, background: game_color, border_thickness_right: 1, border_color_right: W3DHub::BORDER_COLOR) do
background 0x55_000000 # Game Logo
logo_image_path = Cache.package_path(game.category, game.id, "logo.png", "")
# Game Banner if File.exist?(logo_image_path)
image_path = "#{GAME_ROOT_PATH}/media/banners/#{game.id}.png" image logo_image_path, width: 1.0
if File.exist?(image_path)
image image_path, width: 1.0
else else
banner game.name unless File.exist?(image_path) banner game.name unless File.exist?(logo_image_path)
end end
stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do
@@ -128,7 +156,7 @@ class W3DHub
flow(width: 1.0, height: 22, margin_bottom: 8) do flow(width: 1.0, height: 22, margin_bottom: 8) do
image "#{GAME_ROOT_PATH}/media/ui_icons/#{hash[:icon]}.png", width: 24 if hash[:icon] image "#{GAME_ROOT_PATH}/media/ui_icons/#{hash[:icon]}.png", width: 24 if hash[:icon]
image EMPTY_IMAGE, width: 24 unless hash[:icon] image EMPTY_IMAGE, width: 24 unless hash[:icon]
link key, text_size: 18, enabled: hash.key?(:enabled) ? hash[:enabled] : true do link key, text_size: 22, enabled: hash.key?(:enabled) ? hash[:enabled] : true do
hash[:block]&.call hash[:block]&.call
end end
end end
@@ -138,7 +166,7 @@ class W3DHub
game.web_links.each do |item| game.web_links.each do |item|
flow(width: 1.0, height: 22, margin_bottom: 8) do flow(width: 1.0, height: 22, margin_bottom: 8) do
image "#{GAME_ROOT_PATH}/media/ui_icons/share1.png", width: 24 image "#{GAME_ROOT_PATH}/media/ui_icons/share1.png", width: 24
link item.name, text_size: 18 do link item.name, text_size: 22 do
W3DHub.url(item.uri) W3DHub.url(item.uri)
end end
end end
@@ -148,7 +176,7 @@ class W3DHub
if game.channels.count > 1 if game.channels.count > 1
# Release channel # Release channel
inscription I18n.t(:"games.game_version"), width: 1.0, text_align: :center para I18n.t(:"games.game_version"), width: 1.0, text_align: :center
flow(width: 1.0, height: 48) do flow(width: 1.0, height: 48) do
# background 0xff_444411 # background 0xff_444411
@@ -159,17 +187,17 @@ class W3DHub
end end
# Play buttons # Play buttons
flow(width: 1.0, height: 48, padding_top: 6, margin_bottom: 16) do flow(width: 1.0, height: 52, padding_top: 6) do
# background 0xff_551100 # background 0xff_551100
if Store.application_manager.installed?(game.id, channel.id) if Store.application_manager.installed?(game.id, channel.id)
if Store.application_manager.updateable?(game.id, channel.id) if Store.application_manager.updateable?(game.id, channel.id)
button "<b>#{I18n.t(:"interface.install_update")}</b>", fill: true, text_size: 32, **UPDATE_BUTTON do button "<b>#{I18n.t(:"interface.install_update")}</b>", fill: true, text_size: 30, **UPDATE_BUTTON do
Store.application_manager.update(game.id, channel.id) Store.application_manager.update(game.id, channel.id)
end end
else else
play_now_server = Store.application_manager.play_now_server(game.id, channel.id) play_now_server = Store.application_manager.play_now_server(game.id, channel.id)
play_now_button = button "<b>#{I18n.t(:"interface.play")}</b>", fill: true, text_size: 32, enabled: !play_now_server.nil? do play_now_button = button "<b>#{I18n.t(:"interface.play")}</b>", fill: true, text_size: 30, enabled: !play_now_server.nil? do
Store.application_manager.play_now(game.id, channel.id) Store.application_manager.play_now(game.id, channel.id)
end end
@@ -185,34 +213,52 @@ class W3DHub
end end
button get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), tip: I18n.t(:"games.game_options"), image_height: 32, margin_left: 0 do |btn| button get_image("#{GAME_ROOT_PATH}/media/ui_icons/gear.png"), tip: I18n.t(:"games.game_options"), image_height: 32, margin_left: 0 do |btn|
items = [] menu(parent: btn) do
menu_item(I18n.t(:"games.game_settings")) do
if game.uses_engine_cfg?
push_state(States::GameSettingsDialog, app_id: game.id, channel: channel.id)
else
Store.application_manager.wwconfig(game.id, channel.id)
end
end
items << { label: I18n.t(:"games.game_settings"), block: proc { push_state(States::GameSettingsDialog, app_id: game.id, channel: channel.id) } } #, block: proc { Store.application_manager.wwconfig(game.id, channel.id) } } if W3DHub.unix?
# items << { label: I18n.t(:"games.game_settings"), block: proc { Store.application_manager.settings(game.id, channel.id) } } menu_item(I18n.t(:"games.wine_configuration")) do
items << { label: I18n.t(:"games.wine_configuration"), block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix? Store.application_manager.wine_configuration(game.id, channel.id)
items << { label: I18n.t(:"games.game_modifications"), block: proc { populate_game_modifications(game, channel) } } unless Store.offline_mode end
if game.id != "ren" end
items << { label: I18n.t(:"games.repair_installation"), block: proc { Store.application_manager.repair(game.id, channel.id) } } unless Store.offline_mode
items << { label: I18n.t(:"games.uninstall_game"), block: proc { Store.application_manager.uninstall(game.id, channel.id) } } unless Store.offline_mode
end
# From gui_state_ext.rb unless Store.offline_mode
# TODO: Implement in engine proper if W3DHUB_DEVELOPER
menu(btn, items: items) menu_item(I18n.t(:"games.game_modifications")) do
populate_game_modifications(game, channel)
end
end
if game.id != "ren"
menu_item(I18n.t(:"games.repair_installation")) do
Store.application_manager.repair(game.id, channel.id)
end
menu_item(I18n.t(:"games.uninstall_game")) do
Store.application_manager.uninstall(game.id, channel.id)
end
end
end
end.show
end end
else else
installing = Store.application_manager.task?(:installer, game.id, channel.id) installing = Store.application_manager.task?(:installer, game.id, channel.id)
unless game.id == "ren" unless game.id == "ren"
button "<b>#{I18n.t(:"interface.install")}</b>", fill: true, margin_right: 8, text_size: 32, enabled: !installing do |button| button "<b>#{I18n.t(:"interface.install")}</b>", fill: true, margin_right: 8, text_size: 30, enabled: !installing do |button|
button.enabled = false button.enabled = false
@import_button.enabled = false @import_button.enabled = false
Store.application_manager.install(game.id, channel.id) Store.application_manager.install(game.id, channel.id)
end end
end end
@import_button = button "<b>#{I18n.t(:"interface.import")}</b>", fill: true, margin_left: 8, text_size: 32, enabled: !installing do @import_button = button "<b>#{I18n.t(:"interface.import")}</b>", fill: true, margin_left: 8, text_size: 30, enabled: !installing do
Store.application_manager.import(game.id, channel.id) Store.application_manager.import(game.id, channel.id)
end end
end end
@@ -225,7 +271,7 @@ class W3DHub
# Height should match Game Banner container height # Height should match Game Banner container height
stack(width: 1.0, padding: 16) do stack(width: 1.0, padding: 16) do
title "About #{game.name}", border_bottom_color: 0xff_666666, border_bottom_thickness: 1, width: 1.0 title "About #{game.name}", border_bottom_color: 0xff_666666, border_bottom_thickness: 1, width: 1.0
para "Command & Conquer: Tiberian Sun is a 1999 real-time stretegy video game by Westwood Studios, published by Electronic Arts, releaseed exclusively for Microsoft Windows on August 27th, 1999. The game is the sequel to the 1995 game Command & Conquer. It featured new semi-3D graphics, a more futuristic sci-fi setting, and new gameplay features such as vehicles capable of hovering and burrowing.", width: 1.0, text_size: 20 para "Command & Conquer: Tiberian Sun is a 1999 real-time stretegy video game by Westwood Studios, published by Electronic Arts, releaseed exclusively for Microsoft Windows on August 27th, 1999. The game is the sequel to the 1995 game Command & Conquer. It featured new semi-3D graphics, a more futuristic sci-fi setting, and new gameplay features such as vehicles capable of hovering and burrowing.", width: 1.0
end end
end end
@@ -282,11 +328,6 @@ class W3DHub
def populate_all_games_view def populate_all_games_view
@game_page_container.clear do @game_page_container.clear do
background 0x88_353535
@game_page_container.style.background_image_color = 0x88_353535
@game_page_container.style.default[:background_image_color] = 0x88_353535
@game_page_container.update_background_image
@focused_game = nil @focused_game = nil
@focused_channel = nil @focused_channel = nil
@@ -323,9 +364,9 @@ class W3DHub
flow(width: 1.0, fill: true, scroll: true) do flow(width: 1.0, fill: true, scroll: true) do
Store.applications.games.each do |game| Store.applications.games.each do |game|
stack(width: 166, height: 224, margin: 8, background: 0x88_151515, border_color: game.color, border_thickness: 1) do stack(width: 166, height: 224, margin: 8, background: 0x88_151515, border_color: game.color, border_thickness: 1) do
flow(width: 1.0, height: 24, padding: 8) do flow(width: 1.0, height: 28, padding: 8) do
para "Favorite", fill: true para "Favorite", fill: true
toggle_button checked: Store.application_manager.favorite?(game.id), text_size: 18, padding_top: 3, padding_right: 3, padding_bottom: 3, padding_left: 3 do |btn| toggle_button checked: Store.application_manager.favorite?(game.id), height: 18, padding_top: 3, padding_right: 3, padding_bottom: 3, padding_left: 3 do |btn|
Store.application_manager.favorive(game.id, btn.value) Store.application_manager.favorive(game.id, btn.value)
Store.settings.save_settings Store.settings.save_settings
@@ -334,7 +375,7 @@ class W3DHub
end end
container = stack(fill: true, width: 1.0, padding: 8) do container = stack(fill: true, width: 1.0, padding: 8) do
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{game.id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png" image_path = File.exist?("#{CACHE_PATH}/#{game.id}.png") ? "#{CACHE_PATH}/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
flow(width: 1.0, margin_top: 8) do flow(width: 1.0, margin_top: 8) do
flow(fill: true) flow(fill: true)
image image_path, width: 0.5 image image_path, width: 0.5
@@ -376,10 +417,11 @@ class W3DHub
return false unless news return false unless news
news.items[0..15].each do |item| news.items[0..15].each do |item|
Cache.fetch(uri: item.image, async: false) Cache.fetch(uri: item.image, async: false, backend: :w3dhub)
end end
@game_news[game.id] = news @game_news[game.id] = news
@game_news["#{game.id}_expires"] = Gosu.milliseconds + (60 * 60 * 1000) # 1 hour (in ms)
"game_news_#{game.id}" "game_news_#{game.id}"
end end
@@ -388,6 +430,9 @@ class W3DHub
return unless @focused_game == game return unless @focused_game == game
if (feed = @game_news[game.id]) if (feed = @game_news[game.id])
game_color = Gosu::Color.new(game.color)
game_color.alpha = 0xaa
@game_news_container.clear do @game_news_container.clear do
# Patch Notes # Patch Notes
if false # Patch notes if false # Patch notes
@@ -417,25 +462,22 @@ class W3DHub
feed.items.sort_by { |i| i.timestamp }.reverse[0..9].each do |item| feed.items.sort_by { |i| i.timestamp }.reverse[0..9].each do |item|
image_path = Cache.path(item.image) image_path = Cache.path(item.image)
flow(width: 1.0, max_width: 869, height: 200, margin: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do flow(width: 1.0, max_width: 869, height: 200, margin: 8, background: game_color, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
background 0x88_000000 if File.file?(image_path)
image image_path, height: 1.0
end
image image_path, height: 1.0 stack(fill: true, height: 1.0, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(game.color))) do
stack(fill: true, height: 1.0, background: 0x44_000000, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(game.color))) do
tagline "<b>#{item.title}</b>", width: 1.0 tagline "<b>#{item.title}</b>", width: 1.0
inscription item.blurb.gsub(/\n+/, "\n").strip[0..1024], fill: true para item.blurb.gsub(/\n+/, "\n").strip[0..1024], fill: true
flow(fill: true) flow(width: 1.0, height: 36, margin_top: 8) do
flow(width: 1.0, height: 32, margin_top: 8) do
stack(fill: true, height: 1.0) do stack(fill: true, height: 1.0) do
flow(fill: true) flow(fill: true)
inscription "#{item.author}#{item.timestamp.strftime("%Y-%m-%d")}" para "#{item.author}#{item.timestamp.strftime("%Y-%m-%d")}"
flow(fill: true)
end end
button I18n.t(:"games.read_more"), width: 1.0, max_width: 128, padding_top: 4, padding_bottom: 4 do button I18n.t(:"games.read_more"), width: 1.0, max_width: 128, padding_top: 4, padding_bottom: 4, margin_left: 0, margin_top: 0, margin_bottom: 0, margin_right: 0 do
W3DHub.url(item.uri) W3DHub.url(item.uri)
end end
end end
@@ -472,10 +514,10 @@ class W3DHub
@game_events_container.clear do @game_events_container.clear do
events.flatten.each do |event| 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 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 0xaa_222222 background 0x44_000000
title event.title, width: 1.0, text_align: :center title event.title, width: 1.0, text_align: :center
tagline event.start_time.strftime("%A"), text_size: 36, 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 caption event.start_time.strftime("%B %e, %Y %l:%M %p"), width: 1.0, text_align: :center
end end
end end
@@ -508,12 +550,12 @@ class W3DHub
stack(width: 0.75, height: 1.0) do stack(width: 0.75, height: 1.0) do
stack(width: 1.0, height: 128 - 28) do stack(width: 1.0, height: 128 - 28) do
link(mod[:name]) { W3DHub.url(mod[:url]) } link(mod[:name]) { W3DHub.url(mod[:url]) }
inscription "Author: #{mod[:author]} | #{mod[:type]} | #{mod[:subtype]}" para "Author: #{mod[:author]} | #{mod[:type]} | #{mod[:subtype]}"
para mod[:description][0..180] para mod[:description][0..180]
end end
flow(width: 1.0, height: 28, padding: 4) do flow(width: 1.0, height: 28, padding: 4) do
inscription "Version", width: 0.25, text_align: :center para "Version", width: 0.25, text_align: :center
list_box items: mod[:versions], width: 0.5, enabled: mod[:versions].size > 1, padding_top: 0, padding_bottom: 0 list_box items: mod[:versions], width: 0.5, enabled: mod[:versions].size > 1, padding_top: 0, padding_bottom: 0
button "Install", width: 0.25, padding_top: 0, padding_bottom: 0 button "Install", width: 0.25, padding_top: 0, padding_bottom: 0
end end

View File

@@ -4,11 +4,9 @@ class W3DHub
def setup def setup
body.clear do body.clear do
flow(width: 1.0, height: 1.0, padding: 32) do flow(width: 1.0, height: 1.0, padding: 32) do
background 0xff_252535 background 0xaa_25253f
stack(width: 0.28) stack(width: 610, height: 380, v_align: :center, h_align: :center) do
stack(width: 0.48) do
flow(width: 1.0) do flow(width: 1.0) do
stack(width: 0.4) stack(width: 0.4)
image "#{GAME_ROOT_PATH}/media/icons/w3dhub.png", width: 0.20 image "#{GAME_ROOT_PATH}/media/icons/w3dhub.png", width: 0.20
@@ -20,14 +18,14 @@ class W3DHub
@username = edit_line "", width: 0.75, autofocus: true, focus: true @username = edit_line "", width: 0.75, autofocus: true, focus: true
end end
flow(width: 1.0) do flow(width: 1.0, margin_top: 8) do
tagline "Password", width: 0.25, text_align: :right tagline "Password", width: 0.25, text_align: :right
@password = edit_line "", width: 0.75, type: :password @password = edit_line "", width: 0.75, type: :password
end end
flow(width: 1.0) do flow(width: 1.0) do
tagline "", width: 0.25 tagline "", width: 0.25
button "Log In" do |btn| @action_button = button "Log In" do |btn|
@username.enabled = false @username.enabled = false
@password.enabled = false @password.enabled = false
btn.enabled = false btn.enabled = false
@@ -46,8 +44,10 @@ class W3DHub
Store.settings[:account][:data] = account Store.settings[:account][:data] = account
Store.settings.save_settings Store.settings.save_settings
Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false) if account if account
applications = Api.applications if account Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false, backend: :w3dhub)
applications = Api._applications
end
end end
[account, applications] [account, applications]
@@ -81,7 +81,7 @@ class W3DHub
if Store.account if Store.account
BackgroundWorker.foreground_job( BackgroundWorker.foreground_job(
-> { Cache.fetch(uri: Store.account.avatar_uri, async: false) }, -> { Cache.fetch(uri: Store.account.avatar_uri, async: false, backend: :w3dhub) },
->(result) { ->(result) {
populate_account_info populate_account_info
page(W3DHub::Pages::Games) page(W3DHub::Pages::Games)
@@ -90,31 +90,61 @@ class W3DHub
end end
end end
def populate_account_info def button_down(id)
@host.instance_variable_get(:"@account_container").clear do case id
stack(width: 0.7, height: 1.0) do when Gosu::KB_TAB
tagline "<b>#{Store.account.username}</b>" if @username.focused?
window.current_state.request_focus(@password)
flow(width: 1.0) do else
link(I18n.t(:"interface.log_out"), text_size: 16, width: 0.5) { depopulate_account_info } window.current_state.request_focus(@username)
link I18n.t(:"interface.profile"), text_size: 16, width: 0.49 do
W3DHub.url("https://secure.w3dhub.com/forum/index.php?showuser=#{Store.account.id}")
end
end
end end
when Gosu::KB_ENTER, Gosu::KB_RETURN
@action_button.enabled? && @action_button.clicked_left_mouse_button(@action_button, 0, 0)
end
end
def populate_account_info
return if Store.offline_mode
@host.instance_variable_get(:"@account_container").clear do
flow(fill: true, height: 1.0) do flow(fill: true, height: 1.0) do
flow(fill: true) # Fill empty space to push image over to container edge avatar_image = begin
avatar_image = get_image(Cache.path(Store.account.avatar_uri)) get_image(Cache.path(Store.account.avatar_uri))
rescue
get_image("#{GAME_ROOT_PATH}/media/icons/default_icon.png")
end
mask_image = get_image("#{GAME_ROOT_PATH}/media/textures/circle_mask.png") mask_image = get_image("#{GAME_ROOT_PATH}/media/textures/circle_mask.png")
composite_image = Gosu.render(256, 256) do composite_image = Gosu.render(256, 256) do
avatar_image.draw(0, 0, 0) scale = 1.0
if avatar_image.width > avatar_image.height
# avatar image is wider than tall, use `height` for scaling to ensure we fill the canvas
scale = 256.0 / avatar_image.height
elsif avatar_image.width < avatar_image.height
# avatar image is taller than wide, use `width` for scaling to ensure we fill the canvas
scale = 256.0 / avatar_image.width
else
# avatar image is square, use width for scale to ensure we fit to the canvas
scale = 256.0 / avatar_image.width
end
# Position image center in middle of composite
avatar_image.draw_rot(128, 128, 0, 0, 0.5, 0.5, scale, scale)
# Render mask image with mode :multiply so we get a clean circle cutout of the scaled avatar image
mask_image.draw(0, 0, 1, 1, 1, 0xff_ffffff, :multiply) mask_image.draw(0, 0, 1, 1, 1, 0xff_ffffff, :multiply)
end end
image composite_image, width: 1.0 image composite_image, width: 1.0
end end
stack(width: 0.7, height: 1.0, margin_left: 8) do
link Store.account.username, text_size: 24, font: BOLD_FONT, tip: I18n.t(:"interface.profile"), margin_top: 16, width: 1.0, text_wrap: :none do
W3DHub.url("https://secure.w3dhub.com/forum/index.php?showuser=#{Store.account.id}")
end
link(I18n.t(:"interface.log_out"), text_size: 22) { depopulate_account_info }
end
end end
end end
@@ -124,7 +154,7 @@ class W3DHub
Store.account = nil Store.account = nil
BackgroundWorker.foreground_job( BackgroundWorker.foreground_job(
-> { Api.applications }, -> { Api._applications },
lambda do |applications| lambda do |applications|
if applications if applications
Store.applications = applications Store.applications = applications
@@ -137,8 +167,8 @@ class W3DHub
tagline "<b>#{I18n.t(:"interface.not_logged_in")}</b>", text_wrap: :none tagline "<b>#{I18n.t(:"interface.not_logged_in")}</b>", text_wrap: :none
flow(width: 1.0) do flow(width: 1.0) do
link(I18n.t(:"interface.log_in"), text_size: 16, width: 0.5) { page(W3DHub::Pages::Login) } link(I18n.t(:"interface.log_in"), text_size: 22, width: 0.5) { page(W3DHub::Pages::Login) }
link I18n.t(:"interface.register"), text_size: 16, width: 0.49 do link I18n.t(:"interface.register"), text_size: 22, width: 0.49 do
W3DHub.url("https://secure.w3dhub.com/forum/index.php?app=core&module=global&section=register") W3DHub.url("https://secure.w3dhub.com/forum/index.php?app=core&module=global&section=register")
end end
end end

View File

@@ -8,7 +8,7 @@ class W3DHub
@selected_server ||= nil @selected_server ||= nil
@selected_server_container ||= nil @selected_server_container ||= nil
@selected_color = 0xff_666655 @selected_color = 0xaa_666655
@filters = Store.settings[:server_list_filters] || {} @filters = Store.settings[:server_list_filters] || {}
@filter_region = Store.settings[:server_list_region] || "Any" # "Any", "North America", "Europe" @filter_region = Store.settings[:server_list_region] || "Any" # "Any", "North America", "Europe"
@@ -22,14 +22,14 @@ class W3DHub
stack(width: 1.0, height: 1.0, padding: 8) do stack(width: 1.0, height: 1.0, padding: 8) do
background 0xaa_252525 background 0xaa_252525
stack(width: 1.0, height: 18) do stack(width: 1.0, height: 22) do
inscription "<b>#{I18n.t(:"server_browser.filters")}</b>" para "<b>#{I18n.t(:"server_browser.filters")}</b>", font: BOLD_FONT
end end
flow(width: 1.0, height: 32) do flow(width: 1.0, height: 36) do
flow(width: 128, height: 1.0) do flow(width: 128, height: 1.0) do
# para I18n.t(:"server_browser.region"), width: 0.5 # para I18n.t(:"server_browser.region"), width: 0.5
list_box items: ["Any", "North America", "Europe"], choose: Store.settings[:server_list_region], width: 1.0, height: 1.0, padding_top: 4, padding_bottom: 4 do |value| list_box items: ["Any", "North America", "Europe", "Asia"], choose: Store.settings[:server_list_region], width: 1.0, height: 1.0, padding_top: 4, padding_bottom: 4 do |value|
@filter_region = value @filter_region = value
Store.settings[:server_list_region] = @filter_region Store.settings[:server_list_region] = @filter_region
Store.settings.save_settings Store.settings.save_settings
@@ -43,7 +43,7 @@ class W3DHub
app = Store.applications.games.find { |a| a.id == app_id.to_s } app = Store.applications.games.find { |a| a.id == app_id.to_s }
next unless app next unless app
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{app_id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{app_id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png" image_path = File.exist?("#{CACHE_PATH}/#{app.id}.png") ? "#{CACHE_PATH}/#{app.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
image image_path, tip: "#{app.name}", height: 1.0, image image_path, tip: "#{app.name}", height: 1.0,
border_thickness_bottom: 1, border_color_bottom: 0x00_000000, border_thickness_bottom: 1, border_color_bottom: 0x00_000000,
@@ -63,23 +63,17 @@ class W3DHub
populate_server_list populate_server_list
end end
end end
# button get_image("#{GAME_ROOT_PATH}/media/ui_icons/return.png"), tip: I18n.t(:"server_browser.refresh"), image_height: 1.0, margin_left: 16, padding_left: 2, padding_right: 2, padding_top: 2, padding_bottom: 2 do
# fetch_server_list
# end
flow(fill: true)
button "Direct Connect", height: 1.0, padding_top: 4, padding_bottom: 4 do
push_state(W3DHub::States::DirectConnectDialog)
end
end end
flow(min_width: 372, width: 0.38, max_width: 512, height: 1.0) do |container| flow(min_width: 372, width: 0.38, max_width: 512, height: 1.0) do |container|
button "Direct Connect", height: 1.0, padding_top: 4, padding_bottom: 4 do
push_state(W3DHub::States::DirectConnectDialog)
end
flow(fill: true) flow(fill: true)
inscription "#{I18n.t(:"server_browser.nickname")}:" para "#{I18n.t(:"server_browser.nickname")}:"
@nickname_label = inscription "#{Store.settings[:server_list_username]}" @nickname_label = para "#{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 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 player name
W3DHub.prompt_for_nickname( W3DHub.prompt_for_nickname(
@@ -105,23 +99,23 @@ class W3DHub
# Players # Players
# Ping # Ping
flow(width: 1.0, height: 24) do flow(width: 1.0, height: 24) do
stack(width: 48, padding: 4) do stack(width: 56, padding: 4) do
end end
stack(width: 0.45, height: 1.0) do stack(width: 0.45, height: 1.0) do
para "<b>#{I18n.t(:"server_browser.hostname")}</b>", text_wrap: :none, width: 1.0 para "<b>#{I18n.t(:"server_browser.hostname")}</b>", text_wrap: :none, width: 1.0, font: BOLD_FONT
end end
flow(fill: true, height: 1.0) do flow(fill: true, height: 1.0) do
para "<b>#{I18n.t(:"server_browser.current_map")}</b>", text_wrap: :none, width: 1.0 para "<b>#{I18n.t(:"server_browser.current_map")}</b>", text_wrap: :none, width: 1.0, font: BOLD_FONT
end end
flow(width: 0.11, height: 1.0) do flow(width: 0.11, height: 1.0) do
para "<b>#{I18n.t(:"server_browser.players")}</b>", text_wrap: :none, width: 1.0 para "<b>#{I18n.t(:"server_browser.players")}</b>", text_wrap: :none, width: 1.0, font: BOLD_FONT
end end
stack(width: 48) do stack(width: 56) do
para "<b>#{I18n.t(:"server_browser.ping")}</b>", text_wrap: :none, width: 1.0 para "<b>#{I18n.t(:"server_browser.ping")}</b>", text_wrap: :none, width: 1.0, font: BOLD_FONT
end end
end end
@@ -202,11 +196,11 @@ class W3DHub
def ping_icon(server) def ping_icon(server)
case server.ping case server.ping
when 0..160 when 0..150
@ping_icons[:good] @ping_icons[:good]
when 161..250 when 151..200
@ping_icons[:fair] @ping_icons[:fair]
when 251..1_000 when 201..1_000
@ping_icons[:poor] @ping_icons[:poor]
when 1_001..5_000 when 1_001..5_000
@ping_icons[:bad] @ping_icons[:bad]
@@ -216,7 +210,7 @@ class W3DHub
end end
def ping_tip(server) def ping_tip(server)
server.ping.negative? ? "Ping failed" : "Ping #{server.ping}ms" server.ping == W3DHub::Api::ServerListServer::NO_OR_BAD_PING ? "Ping failed" : "Ping #{server.ping}ms"
end end
def find_element_by_tag(container, tag, list = []) def find_element_by_tag(container, tag, list = [])
@@ -231,12 +225,28 @@ class W3DHub
return list.first return list.first
end end
def refresh_server_list(server) def refresh_server_list(server, mode = :update) # :remove, :refresh_all
if mode == :refresh_all
populate_server_list
return
end
@refresh_server_list = Gosu.milliseconds + 3_000 @refresh_server_list = Gosu.milliseconds + 3_000
@refresh_server = server if @selected_server&.id == server.id @refresh_server = server if @selected_server&.id == server.id
server_container = find_element_by_tag(@server_list_container, server.id) server_container = find_element_by_tag(@server_list_container, server.id)
case mode
when :update
if server.status && !server_container
@server_list_container.append do
create_server_container(server)
end
end
when :remove
@server_list_container.remove(server_container) if server_container
return
end
return unless server_container return unless server_container
game_icon = find_element_by_tag(server_container, :game_icon) game_icon = find_element_by_tag(server_container, :game_icon)
@@ -247,13 +257,14 @@ class W3DHub
player_count = find_element_by_tag(server_container, :player_count) player_count = find_element_by_tag(server_container, :player_count)
server_ping = find_element_by_tag(server_container, :ping) server_ping = find_element_by_tag(server_container, :ping)
server_name.value = "<b>#{server&.status&.name}</b>" game_icon&.value = game_icon(server)
server_channel.value = server.channel server_name&.value = "<b>#{server&.status&.name}</b>"
server_region.value = server.region server_channel&.value = Store.application_manager.channel_name(server.game, server.channel).to_s
server_map.value = server&.status&.map server_region&.value = server.region
player_count.value = "#{server&.status&.player_count}/#{server&.status&.max_players}" server_map&.value = server&.status&.map
server_ping.value = ping_icon(server) player_count&.value = "#{server&.status&.player_count}/#{server&.status&.max_players}"
server_ping.parent.parent.tip = ping_tip(server) server_ping&.value = ping_icon(server)
server_ping&.parent.parent.tip = ping_tip(server)
end end
def update_server_ping(server) def update_server_ping(server)
@@ -270,10 +281,6 @@ class W3DHub
end end
def stylize_selected_server(server_container) def stylize_selected_server(server_container)
server_container.style.server_item_background = server_container.style.default[:background]
server_container.style.server_item_hover_background = server_container.style.hover[:background]
server_container.style.server_item_active_background = server_container.style.active[:background]
server_container.style.background = @selected_color server_container.style.background = @selected_color
server_container.style.default[:background] = @selected_color server_container.style.default[:background] = @selected_color
@@ -285,103 +292,105 @@ class W3DHub
@server_list_container.children.sort_by! do |child| @server_list_container.children.sort_by! do |child|
s = Store.server_list.find { |s| s.id == child.style.tag } s = Store.server_list.find { |s| s.id == child.style.tag }
[s&.status&.player_count, s&.id] [s.status.player_count, -s.ping]
end.reverse!.each_with_index do |child, i| end.reverse!.each_with_index do |child, i|
child.style.background = 0xff_333333 if i.even? next if @selected_server_container && child == @selected_server_container
child.style.background = 0 if i.odd?
child.style.hover[:background] = 0xaa_555566
child.style.hover[:active] = 0xaa_555588
child.style.default[:background] = 0xaa_333333 if i.even?
child.style.default[:background] = 0x00_000000 if i.odd?
end end
@server_list_container.recalculate @server_list_container.recalculate
end end
def populate_server_list def populate_server_list
Store.server_list = Store.server_list.sort_by! { |s| [s&.status&.player_count, s&.id] }.reverse if Store.server_list Store.server_list = Store.server_list.sort_by! { |s| [s.status.player_count, s.id] }.reverse
@server_list_container.clear do @server_list_container.clear do
i = -1
Store.server_list.each do |server| Store.server_list.each do |server|
next unless @filters[server.game.to_sym] create_server_container(server)
next unless server.region == @filter_region || @filter_region == "Any"
# next unless server.channel == "release"
i += 1
server_container = flow(width: 1.0, height: 48, hover: { background: 0xff_555566 }, active: { background: 0xff_555588 }, tag: server.id, tip: ping_tip(server)) do
background 0xff_333333 if i.even?
flow(width: 48, height: 1.0, padding: 4) do
image game_icon(server), height: 1.0, tag: :game_icon
end
stack(width: 0.45, height: 1.0) do
inscription "<b>#{server&.status&.name}</b>", tag: :server_name
flow(width: 1.0, height: 1.0) do
inscription server.channel, margin_right: 64, text_size: 14, tag: :server_channel
inscription server.region, text_size: 14, tag: :server_region
end
end
flow(fill: true, height: 1.0) do
inscription "#{server&.status&.map}", tag: :server_map
end
flow(width: 0.11, height: 1.0) do
inscription "#{server&.status&.player_count}/#{server&.status&.max_players}", tag: :player_count
end
flow(width: 48, height: 1.0, padding: 4) do
image ping_icon(server), height: 1.0, tag: :ping
end
end
def server_container.hit_element?(x, y)
self if hit?(x, y)
end
server_container.subscribe(:clicked_left_mouse_button) do
if @selected_server_container
@selected_server_container.style.background = @selected_server_container.style.server_item_background
@selected_server_container.style.default[:background] = @selected_server_container.style.server_item_background
@selected_server_container.style.hover[:background] = @selected_server_container.style.server_item_hover_background
@selected_server_container.style.active[:background] = @selected_server_container.style.server_item_active_background
end
stylize_selected_server(server_container)
@selected_server_container = server_container
@selected_server = server
BackgroundWorker.foreground_job(
-> { fetch_server_details(server) },
->(result) { populate_server_info(server) if server == @selected_server }
)
end
stylize_selected_server(server_container) if server.id == @selected_server&.id
end end
end end
reorder_server_list
end
def create_server_container(server)
return unless @filters[server.game.to_sym]
return unless server.status
return unless server.region == @filter_region || @filter_region == "Any"
return unless Store.application_manager.channel_name(server.game, server.channel) # can user access required game and channel for this server?
server_container = flow(width: 1.0, height: 56, hover: { background: 0xaa_555566 }, active: { background: 0xaa_555588 }, tag: server.id, tip: ping_tip(server)) do
flow(width: 56, height: 1.0, padding: 4) do
image game_icon(server), height: 1.0, tag: :game_icon
end
stack(width: 0.45, height: 1.0) do
para server&.status&.name, tag: :server_name, font: BOLD_FONT, text_wrap: :none
flow(width: 1.0, height: 1.0) do
para Store.application_manager.channel_name(server.game, server.channel).to_s, width: 172, margin_right: 8, tag: :server_channel
para server.region, tag: :server_region
end
end
flow(fill: true, height: 1.0) do
para "#{server&.status&.map}", tag: :server_map
end
flow(width: 0.11, height: 1.0) do
para "#{server&.status&.player_count}/#{server&.status&.max_players}", tag: :player_count
end
flow(width: 56, height: 1.0, padding: 4) do
image ping_icon(server), height: 1.0, tag: :ping
end
end
def server_container.hit_element?(x, y)
self if hit?(x, y)
end
server_container.subscribe(:clicked_left_mouse_button) do
stylize_selected_server(server_container)
@selected_server_container = server_container
@selected_server = server
reorder_server_list if @selected_server_container
BackgroundWorker.foreground_job(
-> { fetch_server_details(server) },
->(result) { populate_server_info(server) if server == @selected_server }
)
end
stylize_selected_server(server_container) if server.id == @selected_server&.id
end end
def populate_server_info(server) def populate_server_info(server)
@game_server_info_container.clear do @game_server_info_container.clear do
stack(width: 1.0, height: 1.0, padding: 8) do stack(width: 1.0, height: 1.0, padding: 8) do
stack(width: 1.0, height: 220) do stack(width: 1.0, height: 208) do
flow(width: 1.0, height: 0.2) do flow(width: 1.0, height: 34) do
flow(fill: true) flow(fill: true)
image game_icon(server), width: 0.05 image game_icon(server), height: 1.0
tagline server.status.name, text_wrap: :none title server.status.name[0..30], text_wrap: :none
flow(fill: true) flow(fill: true)
end end
flow(width: 1.0, height: 0.2) do flow(width: 1.0, height: 46, margin_top: 16, margin_bottom: 16) do
game_installed = Store.application_manager.installed?(server.game, server.channel) game_installed = Store.application_manager.installed?(server.game, server.channel)
game_updatable = Store.application_manager.updateable?(server.game, server.channel) game_updatable = Store.application_manager.updateable?(server.game, server.channel)
style = server.channel != "release" ? TESTING_BUTTON : {} channel = Store.application_manager.channel(server.game, server.channel)
style = ((channel && channel.user_level.downcase.strip == "public") || server.channel == "release") ? {} : TESTING_BUTTON
flow(fill: true) flow(fill: true)
button "<b>#{I18n.t(:"server_browser.join_server")}</b>", enabled: (game_installed && !game_updatable), **style do button "<b>#{I18n.t(:"server_browser.join_server")}</b>", enabled: (game_installed && !game_updatable), **style do
@@ -401,11 +410,11 @@ class W3DHub
if server.status.password if server.status.password
W3DHub.prompt_for_password( W3DHub.prompt_for_password(
accept_callback: proc do |password| accept_callback: proc do |password|
W3DHub.join_server(server, password) W3DHub.join_server(server: server, password: password)
end end
) )
else else
W3DHub.join_server(server, nil) W3DHub.join_server(server: server)
end end
end end
) )
@@ -413,48 +422,49 @@ class W3DHub
if server.status.password if server.status.password
W3DHub.prompt_for_password( W3DHub.prompt_for_password(
accept_callback: proc do |password| accept_callback: proc do |password|
W3DHub.join_server(server, password) W3DHub.join_server(server: server, password: password)
end end
) )
else else
W3DHub.join_server(server, nil) W3DHub.join_server(server: server)
end end
end end
end end
if Store.developer_mode if W3DHUB_DEVELOPER
list_box(items: (1..12).to_a.map(&:to_s), margin_left: 16, **TESTING_BUTTON) client_instances = list_box(items: (1..12).to_a.map(&:to_s), margin_left: 16, width: 72, tip: "Number of game clients", enabled: (game_installed && !game_updatable), **TESTING_BUTTON)
button "Multijoin", tip: "Launch multiple clients with configured username_\#{number}", **TESTING_BUTTON, enabled: true button("Multijoin", tip: "Launch multiple clients with configured username_\#{number}", enabled: (game_installed && !game_updatable), **TESTING_BUTTON) do
username = Store.settings[:server_list_username]
client_instances.value.to_i.times do |i|
W3DHub.join_server(server: server, username: format("%s_%d", username, i), multi: true)
end
end
end end
flow(fill: true) flow(fill: true)
end end
# Server Info # Server Info
stack(width: 1.0, fill: true, margin_top: 16) do stack(width: 1.0, fill: true, margin_bottom: 16) do
flow(width: 1.0) do flow(width: 1.0) do
inscription "<b>#{I18n.t(:"server_browser.game")}</b>", width: 0.28, text_wrap: :none para "<b>#{I18n.t(:"server_browser.game")}</b>", width: 0.12, text_wrap: :none, font: BOLD_FONT
inscription "#{game_name(server.game)} (#{server.channel})", width: 0.71, text_wrap: :none para "#{game_name(server.game)} (#{server.channel})", width: 0.71, text_wrap: :none
end end
flow(width: 1.0) do flow(width: 1.0) do
inscription "<b>#{I18n.t(:"server_browser.map")}</b>", width: 0.28, text_wrap: :none para "<b>#{I18n.t(:"server_browser.map")}</b>", width: 0.12, text_wrap: :none, font: BOLD_FONT
inscription server.status.map, width: 0.71, text_wrap: :none para server.status.map, width: 0.71, text_wrap: :none
end end
flow(width: 1.0) do flow(width: 1.0) do
inscription "<b>#{I18n.t(:"server_browser.max_players")}</b>", width: 0.28, text_wrap: :none para "<b>#{I18n.t(:"server_browser.time")}</b>", width: 0.12, text_wrap: :none, font: BOLD_FONT
inscription "#{server.status.max_players}", width: 0.71, text_wrap: :none para formatted_rentime(server.status.started), text_wrap: :none
end
flow(width: 1.0) do unless server.status.remaining =~ /00:00:00|00.00.00/
inscription "<b>#{I18n.t(:"server_browser.time")}</b>", width: 0.28, text_wrap: :none para "<b>#{I18n.t(:"server_browser.remaining")}</b>", margin_left: 16, margin_right: 8, text_wrap: :none, font: BOLD_FONT
inscription formatted_rentime(server.status.started), width: 0.71, text_wrap: :none para "#{server.status.remaining}", text_wrap: :none
end end
flow(width: 1.0) do
inscription "<b>#{I18n.t(:"server_browser.remaining")}</b>", width: 0.28, text_wrap: :none
inscription "#{server.status.remaining}", width: 0.71, text_wrap: :none
end end
end end
end end
@@ -462,9 +472,9 @@ class W3DHub
game_balance = server_game_balance(server) game_balance = server_game_balance(server)
# Game score and balance display # Game score and balance display
flow(width: 1.0, height: 48, border_thickness_bottom: 2, border_color_bottom: 0x44_ffffff) do flow(width: 1.0, height: 52, border_thickness_bottom: 2, border_color_bottom: 0x44_ffffff) do
stack(fill: true, height: 1.0) do stack(fill: true, height: 1.0) do
para "<b>#{server.status.teams[0].name} (#{server.status.players.select { |pl| pl.team == 0 }.count})</b>", width: 1.0, text_align: :center para "#{server.status.teams[0].name} (#{server.status.players.select { |pl| pl.team == 0 }.count})", width: 1.0, text_align: :center, font: BOLD_FONT
para formatted_score(game_balance[:team_0_score].to_i), width: 1.0, text_align: :center para formatted_score(game_balance[:team_0_score].to_i), width: 1.0, text_align: :center
end end
@@ -479,7 +489,7 @@ class W3DHub
end end
stack(fill: true, height: 1.0) do stack(fill: true, height: 1.0) do
para "<b>#{server.status.teams[1].name} (#{server.status.players.select { |pl| pl.team == 1 }.count})</b>", width: 1.0, text_align: :center para "#{server.status.teams[1].name} (#{server.status.players.select { |pl| pl.team == 1 }.count})", width: 1.0, text_align: :center, font: BOLD_FONT
para formatted_score(game_balance[:team_1_score].to_i), width: 1.0, text_align: :center para formatted_score(game_balance[:team_1_score].to_i), width: 1.0, text_align: :center
end end
end end
@@ -488,15 +498,15 @@ class W3DHub
flow(width: 1.0, fill: true, scroll: true) do flow(width: 1.0, fill: true, scroll: true) do
stack(width: 0.5) do stack(width: 0.5) do
server.status.players.select { |ply| ply.team == 0 }.sort_by { |ply| ply.score }.reverse.each_with_index do |player, i| server.status.players.select { |ply| ply.team == 0 }.sort_by { |ply| ply.score }.reverse.each_with_index do |player, i|
flow(width: 1.0, height: 18) do flow(width: 1.0, height: 26) do
background 0xff_333333 if i.even? background 0xaa_333333 if i.even?
stack(width: 0.6, height: 1.0) do stack(width: 0.6, height: 1.0) do
inscription player.nick, text_size: 14, text_wrap: :none para player.nick, text_wrap: :none
end end
stack(width: 0.4, height: 1.0) do stack(width: 0.4, height: 1.0) do
inscription formatted_score(player.score), text_size: 14, width: 1.0, text_align: :right, text_wrap: :none para formatted_score(player.score), width: 1.0, text_align: :right, text_wrap: :none
end end
end end
end end
@@ -504,15 +514,15 @@ class W3DHub
stack(width: 0.5, border_thickness_left: 2, border_color_left: 0xff_000000) do stack(width: 0.5, border_thickness_left: 2, border_color_left: 0xff_000000) do
server.status.players.select { |ply| ply.team == 1 }.sort_by { |ply| ply.score }.reverse.each_with_index do |player, i| server.status.players.select { |ply| ply.team == 1 }.sort_by { |ply| ply.score }.reverse.each_with_index do |player, i|
flow(width: 1.0, height: 18) do flow(width: 1.0, height: 26) do
background 0xff_333333 if i.even? background 0xaa_333333 if i.even?
stack(width: 0.6, height: 1.0) do stack(width: 0.6, height: 1.0) do
inscription player.nick, text_size: 14, text_wrap: :none para player.nick, text_wrap: :none
end end
stack(width: 0.4, height: 1.0) do stack(width: 0.4, height: 1.0) do
inscription formatted_score(player.score), text_size: 14, width: 1.0, text_align: :right, text_wrap: :none para formatted_score(player.score), width: 1.0, text_align: :right, text_wrap: :none
end end
end end
end end
@@ -530,7 +540,7 @@ class W3DHub
end end
def game_icon(server) def game_icon(server)
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{server.game.nil? ? 'ren' : server.game}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{server.game.nil? ? 'ren' : server.game}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png" image_path = File.exist?("#{CACHE_PATH}/#{server.game.nil? ? 'ren' : server.game}.png") ? "#{CACHE_PATH}/#{server.game.nil? ? 'ren' : server.game}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
if server.status.password if server.status.password
@server_locked_icons[server.game] ||= Gosu.render(96, 96) do @server_locked_icons[server.game] ||= Gosu.render(96, 96) do

View File

@@ -3,81 +3,57 @@ class W3DHub
class Settings < Page class Settings < Page
def setup def setup
body.clear do 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 background 0xaa_252525
para "<b>Language</b>" stack(width: 1.0, fill: true, max_width: 720, h_align: :center, scroll: true) do
flow(width: 1.0, height: 0.12) do stack(width: 1.0, height: 112) do
para "<b>Launcher Language</b>", width: 0.249, margin_left: 32, margin_top: 12 tagline "Launcher Language"
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, 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 para "Select the UI language you'd like to use in the W3D Hub Launcher.", margin_left: 16
inscription "Select the UI language you'd like to use in the W3D Hub Launcher."
end end
end
para "<b>Folder Paths</b>", margin_top: 8, padding_top: 8, border_thickness_top: 2, border_color_top: 0xee_ffffff, width: 1.0
stack(width: 1.0, height: 0.3) do
flow(width: 1.0, height: 0.5) do
para "<b>App Install Folder</b>", width: 0.249, margin_left: 32, margin_top: 12
stack(width: 0.75) do stack(width: 1.0, height: 200, margin_top: 16) do
@app_install_dir_input = edit_line Store.settings[:app_install_dir], width: 1.0 tagline "Launcher Directories"
inscription "The folder into which new games and apps will be installed by the launcher" 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
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
flow(width: 1.0, margin_top: 16) do if W3DHub.unix?
para "<b>Package Cache Folder</b>", width: 0.249, margin_left: 32, margin_top: 12 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
stack(width: 0.75) do caption "Wine Prefix", margin_left: 16, margin_top: 16
@package_cache_dir_input = edit_line Store.settings[:package_cache_dir], width: 1.0 flow(width: 1.0, height: 48, margin_left: 16) do
inscription "A folder which will be used to cache downloaded packages used to install games and apps" @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 end
end end
if true # W3DHub.unix? stack(width: 128, max_height: 256, h_align: :center, margin_top: 16, fill: true) do
para "<b>Wine</b>", margin_top: 8, padding_top: 8, border_thickness_top: 2, border_color_top: 0xee_ffffff, width: 1.0 button "Save", width: 1.0 do
flow(width: 1.0, height: 0.12) do save_settings!
para "<b>Wine Command</b>", 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
inscription "Command to use to for Windows compatiblity layer"
end
end
flow(width: 1.0, height: 0.13, margin_top: 16) do
para "<b>Wine Prefix</b>", width: 0.249, margin_left: 32, margin_top: 12
stack(width: 0.75) do
@wine_prefix_toggle = toggle_button checked: Store.settings[:wine_prefix]
inscription "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
end end
end end
end end
@@ -108,7 +84,35 @@ class W3DHub
when "es" when "es"
"Español" "Español"
else else
raise "Unknown language error" logger.warn("W3DHub::Settings") { "Unknown language code: #{string.inspect}" }
"UNKNOWN"
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 end

View File

@@ -71,5 +71,15 @@ class W3DHub
def save_settings def save_settings
File.write(SETTINGS_FILE_PATH, @settings.to_json) File.write(SETTINGS_FILE_PATH, @settings.to_json)
end end
def save_application_cache(json)
File.write(APPLICATIONS_CACHE_FILE_PATH, json)
end
def load_application_cache
JSON.parse(File.read(APPLICATIONS_CACHE_FILE_PATH), symbolize_names: true)
rescue
nil
end
end end
end end

View File

@@ -8,40 +8,40 @@ class W3DHub
theme(W3DHub::THEME) theme(W3DHub::THEME)
background 0xff_252525
@fraction = 0.0 @fraction = 0.0
@w3dhub_logo = get_image("#{GAME_ROOT_PATH}/media/icons/app.png") @w3dhub_logo = get_image("#{GAME_ROOT_PATH}/media/icons/app.png")
@tasks = { @tasks = {
# connectivity_check: { started: false, complete: false }, # HEAD connectivity-check.ubuntu.com or HEAD secure.w3dhub.com? 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 }, refresh_user_token: { started: false, complete: false },
service_status: { started: false, complete: false }, service_status: { started: false, complete: false },
applications: { started: false, complete: false }, applications: { started: false, complete: false },
app_icons: { started: false, complete: false }, app_icons: { started: false, complete: false },
server_list: { started: false, complete: false } app_logos_and_backgrounds: { started: false, complete: false }
} }
@offline_mode = false @offline_mode = false
@task_index = 0 @task_index = 0
stack(width: 1.0, height: 1.0, border_thickness: 1, border_color: W3DHub::BORDER_COLOR) do stack(width: 1.0, height: 1.0, border_thickness: 1, border_color: W3DHub::BORDER_COLOR, background_image: "#{GAME_ROOT_PATH}/media/banners/background.png", background_image_color: 0xff_525252, background_image_mode: :fill) do
stack(width: 1.0, fill: true) do stack(width: 1.0, fill: true) do
end end
stack(width: 1.0, height: 75) do stack(width: 1.0, height: 60) do
@progressbar = progress height: 25, width: 1.0 flow(width: 1.0, height: 26, margin_left: 16, margin_right: 16, margin_bottom: 8, margin_top: 8) do
@status_label = caption "Starting #{I18n.t(:app_name_simple)}...", fill: true
flow(width: 1.0, fill: true, padding_left: 16, padding_right: 16, padding_bottom: 8, padding_top: 8) do para "#{I18n.t(:app_name)} #{W3DHub::VERSION}", text_align: :right
@status_label = caption "Starting #{I18n.t(:app_name_simple)}...", width: 0.5
inscription "#{I18n.t(:app_name)} #{W3DHub::VERSION}", width: 0.5, text_align: :right
end end
@progressbar = progress height: 4, width: 1.0, margin_left: 16, margin_right: 16, margin_bottom: 8
end end
end end
end end
def draw def draw
Gosu.draw_circle(window.width / 2, window.height / 2, @w3dhub_logo.width * (0.6 + Math.cos(Gosu.milliseconds / 1000.0 * Math::PI).abs * 0.05), 128, 0x44_000000, 32) Gosu.draw_circle(window.width / 2, window.height / 2, @w3dhub_logo.width * (0.6 + Math.cos(Gosu.milliseconds / 1000.0 * Math::PI).abs * 0.05), 128, 0xaa_353535, 32)
@w3dhub_logo.draw_rot(window.width / 2, window.height / 2, 32) @w3dhub_logo.draw_rot(window.width / 2, window.height / 2, 32)
super super
@@ -54,10 +54,35 @@ class W3DHub
@progressbar.value = @fraction @progressbar.value = @fraction
load_offline_applications_list if @offline_mode if @offline_mode
load_offline_applications_list
unless Store.applications
@progressbar.value = 0.0
@status_label.value = "<c=f80>Unable to connect to W3D Hub API. No application data cached, unable to continue.</c>"
return
end
end
if @offline_mode || (@progressbar.value >= 1.0 && @task_index == @tasks.size) if @offline_mode || (@progressbar.value >= 1.0 && @task_index == @tasks.size)
pop_state pop_state
# --- Repair/Upgrade settings schema/data
Store.settings[:favorites] ||= {}
# add game colo[u]r and uses_engine_cfg to application data
unless @offline_mode
Store.settings[:games].each do |key, game|
application = Store.applications.games.find { |g| g.id == key.to_s.split("_", 2).first }
next unless application
game[:colour] = "##{application.color.to_s(16)}"
game[:uses_engine_cfg] = application.uses_engine_cfg?
end
end
Store.settings.save_settings
push_state(States::Interface) push_state(States::Interface)
end end
@@ -104,7 +129,7 @@ class W3DHub
Store.settings[:account][:data] = account 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 else
Store.settings[:account] = {} Store.settings[:account] = {}
end end
@@ -114,6 +139,41 @@ class W3DHub
@tasks[:refresh_user_token][:complete] = true @tasks[:refresh_user_token][:complete] = true
end end
def connectivity_check
domains = {
"w3dhub-api.w3d.cyberarm.dev": false,
"s3.w3d.cyberarm.dev": false,
"secure.w3dhub.com": false
}
@status_label.value = "Checking uplink..."
domains.each do |key, value|
begin
Resolv.getaddress(key.to_s)
rescue => e
logger.error(LOG_TAG) {"Failed to resolve hostname: #{key.to_s}"}
logger.error(LOG_TAG) {e}
push_state(
ConfirmDialog,
title: "DNS Resolution Failure",
message: "Failed to resolve: #{key.to_s}\n\nTry disabling VPN or proxy if in use.\n\n\nContinue offline?",
cancel_callback: ->() { window.close },
accept_callback: ->() {
@offline_mode = true
Store.offline_mode = true
@tasks[:connectivity_check][:complete] = true
}
)
# Prevent task from being marked as completed
return false
end
end
@tasks[:connectivity_check][:complete] = true
end
def service_status def service_status
Api.on_thread(:service_status) do |service_status| Api.on_thread(:service_status) do |service_status|
@service_status = service_status @service_status = service_status
@@ -136,13 +196,39 @@ class W3DHub
end end
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 def applications
@status_label.value = I18n.t(:"boot.checking_for_updates") @status_label.value = I18n.t(:"boot.checking_for_updates")
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)
@tasks[:applications][:complete] = true @tasks[:applications][:complete] = true
else else
# FIXME: Failed to retreive! # FIXME: Failed to retreive!
@@ -157,17 +243,21 @@ class W3DHub
def app_icons def app_icons
return unless Store.applications return unless Store.applications
@status_label.value = "Retrieving application icons, this might take a moment..." # I18n.t(:"boot.checking_for_updates")
packages = [] packages = []
Store.applications.games.each do |app| Store.applications.games.each do |app|
packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" } packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" }
end 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 ||= nil
package_details&.each do |package| package_details&.each do |package|
next if package.error?
path = Cache.package_path(package.category, package.subcategory, package.name, package.version) path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
generated_icon_path = "#{GAME_ROOT_PATH}/media/icons/#{package.subcategory}.png" generated_icon_path = "#{CACHE_PATH}/#{package.subcategory}.png"
regenerate = false regenerate = false
@@ -189,6 +279,34 @@ class W3DHub
end end
end end
def app_logos_and_backgrounds
return unless Store.applications
@status_label.value = "Retrieving application image assets, this might take a moment..." # I18n.t(:"boot.checking_for_updates")
packages = []
Store.applications.games.each do |app|
packages << { category: app.category, subcategory: app.id, name: "logo.png", version: "" }
packages << { category: app.category, subcategory: app.id, name: "background.png", version: "" }
end
Api.on_thread(:package_details, packages, :alt_w3dhub) do |package_details|
package_details ||= nil
package_details&.each do |package|
next if package.error?
package_cache_path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
missing_or_broken_image = File.exist?(package_cache_path) ? Digest::SHA256.new.hexdigest(File.binread(package_cache_path)).upcase != package.checksum.upcase : true
Cache.fetch_package(package, proc {}) if missing_or_broken_image
end
@tasks[:app_logos_and_backgrounds][:complete] = true
end
end
def server_list def server_list
@status_label.value = I18n.t(:"server_browser.fetching_server_list") @status_label.value = I18n.t(:"server_browser.fetching_server_list")
@@ -213,6 +331,12 @@ class W3DHub
end end
def load_offline_applications_list def load_offline_applications_list
if (application_cache = Store.settings.load_application_cache)
Store.applications = Api::Applications.new(application_cache.to_json)
return
end
hash = { hash = {
applications: [] applications: []
} }
@@ -230,7 +354,10 @@ class W3DHub
"studio-id": "", "studio-id": "",
channels: [], channels: [],
"web-links": [], "web-links": [],
"extended-data": [{ name: "colour", value: "#353535" }] "extended-data": [
{ name: "colour", value: game[:colour] },
{ name: "usesEngineCfg", value: game[:uses_engine_cfg] },
]
} }
channel = { channel = {
@@ -245,7 +372,7 @@ class W3DHub
hash[:applications] << app unless app_in_array hash[:applications] << app unless app_in_array
end end
Store.applications = Api::Applications.new(hash.to_json) Store.applications = Api::Applications.new(hash.to_json) unless hash[:applications].empty?
end end
end end
end end

View File

@@ -6,22 +6,22 @@ class W3DHub
theme(W3DHub::THEME) theme(W3DHub::THEME)
background 0xee_444444 background 0xaa_525252
stack(width: 1.0, max_width: 720, height: 1.0, max_height: 480, v_align: :center, h_align: :center, background: 0xee_222222) do stack(width: 1.0, max_width: 720, height: 1.0, max_height: 480, v_align: :center, h_align: :center, background: 0xee_222222) do
flow(width: 1.0, height: 0.1, padding: 8) do flow(width: 1.0, height: 48, padding: 8) do
background 0x88_000000 background 0x88_000000
image "#{GAME_ROOT_PATH}/media/ui_icons/question.png", width: 0.04, align: :center, color: 0xaa_ff0000 image "#{GAME_ROOT_PATH}/media/ui_icons/question.png", height: 1.0, align: :center, color: 0xaa_ff0000
tagline "<b>#{@options[:title]}</b>", width: 0.9, text_align: :center title "<b>#{@options[:title]}</b>", width: 0.9, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, height: 0.78, padding: 16) do stack(width: 1.0, fill: true, padding: 16) do
para @options[:message], width: 1.0, text_align: :center para @options[:message], width: 1.0, text_align: :center
end end
flow(width: 1.0, height: 0.1, padding: 8) do flow(width: 1.0, height: 46, padding: 8) do
button "Cancel", width: 0.25 do button "Cancel", width: 0.25 do
pop_state pop_state
@options[:cancel_callback]&.call @options[:cancel_callback]&.call
@@ -29,7 +29,7 @@ class W3DHub
stack(width: 0.5) stack(width: 0.5)
button "Confirm", width: 0.25, background: 0xff_800000, hover: { background: 0xff_d00000 }, active: { background: 0xff_600000, color: 0xff_ffffff } do button "Confirm", width: 0.25, **DANGEROUS_BUTTON do
pop_state pop_state
@options[:accept_callback]&.call @options[:accept_callback]&.call
end end

View File

@@ -7,20 +7,20 @@ class W3DHub
theme(W3DHub::THEME) theme(W3DHub::THEME)
background 0xee_444444 background 0xaa_525252
stack(width: 1.0, max_width: 720, height: 1.0, max_height: 540, v_align: :center, h_align: :center, background: 0xee_222222) do stack(width: 1.0, max_width: 720, height: 1.0, max_height: 576, v_align: :center, h_align: :center, background: 0xee_222222) do
# Title bar # Title bar
flow(width: 1.0, height: 32, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000 background 0x88_000000
image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", height: 1.0, align: :center, color: 0xaa_ffffff
tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center title "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, scroll: true) do stack(width: 1.0, fill: true, scroll: true) do
stack(width: 1.0, height: 66, margin_left: 8, margin_right: 8) do stack(width: 1.0, height: 72, margin_left: 8, margin_right: 8) do
para "Server profiles", text_align: :center, width: 1.0 para "Server profiles", text_align: :center, width: 1.0
flow(width: 1.0, fill: true) do flow(width: 1.0, fill: true) do
@@ -41,7 +41,7 @@ class W3DHub
end end
@server_delete_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/minus.png"), image_height: 1.0, tip: "Remove selected profile" do @server_delete_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/minus.png"), image_height: 1.0, tip: "Remove selected profile" do
push_state(ConfirmDialog, message: "Purge server profile") push_state(ConfirmDialog, title: "Are you sure?", message: "Remove Server Profile: \"#{@server_profiles_list.value}\"?", accept_callback: -> { delete_server_profile(server_profile_from_name(@server_profiles_list.value)) })
end end
@server_edit_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/save.png"), image_height: 1.0, tip: "Edit and save selected profile" do @server_edit_button = button get_image("#{GAME_ROOT_PATH}/media/ui_icons/save.png"), image_height: 1.0, tip: "Edit and save selected profile" do
@@ -51,7 +51,7 @@ class W3DHub
end end
stack(width: 1.0, fill: true, margin_top: 8, padding: 8, border_color: 0xff_111111, border_thickness: 1) do stack(width: 1.0, fill: true, margin_top: 8, padding: 8, border_color: 0xff_111111, border_thickness: 1) do
flow(width: 1.0, height: 66) do flow(width: 1.0, height: 72) do
stack(width: 0.5, height: 1.0) do stack(width: 0.5, height: 1.0) do
para "Nickname:" para "Nickname:"
@server_nickname = edit_line "", width: 1.0, fill: true @server_nickname = edit_line "", width: 1.0, fill: true
@@ -73,7 +73,7 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 66) do flow(width: 1.0, height: 72) do
stack(width: 0.5, height: 1.0) do stack(width: 0.5, height: 1.0) do
para "Server IP or Hostname:" para "Server IP or Hostname:"
@server_hostname = edit_line "", width: 1.0, fill: true @server_hostname = edit_line "", width: 1.0, fill: true
@@ -95,7 +95,7 @@ class W3DHub
end end
end end
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "Game or Mod:" para "Game or Mod:"
flow(width: 1.0, fill: true) do flow(width: 1.0, fill: true) do
@@ -125,7 +125,7 @@ class W3DHub
end end
end end
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "Launch arguments (Optional):" para "Launch arguments (Optional):"
@launch_arguments = edit_line "", width: 1.0, fill: true @launch_arguments = edit_line "", width: 1.0, fill: true
@launch_arguments.subscribe(:changed) do |e| @launch_arguments.subscribe(:changed) do |e|
@@ -135,7 +135,7 @@ class W3DHub
end end
end end
stack(width: 1.0, height: 66) do stack(width: 1.0, height: 72) do
para "IRC Profile (Optional):" para "IRC Profile (Optional):"
flow(width: 1.0, fill: true) do flow(width: 1.0, fill: true) do
@@ -162,7 +162,7 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 40, padding: 8) do flow(width: 1.0, height: 46, padding: 8) do
button "Cancel", width: 0.25 do button "Cancel", width: 0.25 do
pop_state pop_state
end end
@@ -257,7 +257,7 @@ class W3DHub
server_profile: @server_profiles_list.value, server_profile: @server_profiles_list.value,
server_hostname: @server_hostname.value, server_hostname: @server_hostname.value,
server_port: @server_port.value, server_port: @server_port.value,
game: @games_list.value, game_title: @games_list.value,
launch_arguments: @launch_arguments.value, launch_arguments: @launch_arguments.value,
irc_profile: @irc_profiles_list.value irc_profile: @irc_profiles_list.value
} }
@@ -275,6 +275,24 @@ class W3DHub
@changes_made = false @changes_made = false
end end
def delete_server_profile(profile)
index = W3DHub::Store[:asterisk_config].server_profiles.index(profile)
return unless index
W3DHub::Store[:asterisk_config].server_profiles.delete(profile)
W3DHub::Store[:asterisk_config].save_config
@server_profiles_list.items = W3DHub::Store[:asterisk_config].server_profiles.map { |pf| pf.name }
if W3DHub::Store[:asterisk_config].server_profiles.size.positive?
@server_profiles_list.choose = W3DHub::Store[:asterisk_config].server_profiles[index - 1 > 0 ? index - 1 : 0].name
end
end
def server_profile_from_name(name)
W3DHub::Store[:asterisk_config].server_profiles.find { |pf| name == pf.name }
end
def game_from_title(title) def game_from_title(title)
W3DHub::Store[:asterisk_config].games.find { |g| title == g.title } W3DHub::Store[:asterisk_config].games.find { |g| title == g.title }
end end
@@ -362,7 +380,8 @@ class W3DHub
end end
def delete_irc_profile(profile) def delete_irc_profile(profile)
index = W3DHub::Store[:asterisk_config].irc_profiles.index(profile) || 0 index = W3DHub::Store[:asterisk_config].irc_profiles.index(profile)
return unless index
W3DHub::Store[:asterisk_config].irc_profiles.delete(profile) W3DHub::Store[:asterisk_config].irc_profiles.delete(profile)

View File

@@ -1,7 +1,11 @@
class W3DHub class W3DHub
class States class States
class GameSettingsDialog < Dialog class GameSettingsDialog < Dialog
BUTTON_STYLE = { text_size: 18, padding_top: 3, padding_bottom: 3, padding_left: 3, padding_right: 3 } 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 def setup
window.show_cursor = true window.show_cursor = true
@@ -13,13 +17,13 @@ class W3DHub
@game_settings = GameSettings.new(@app_id, @channel) @game_settings = GameSettings.new(@app_id, @channel)
background 0xee_444444 background 0xaa_525252
stack(width: 1.0, max_width: 720, height: 1.0, max_height: 680, v_align: :center, h_align: :center, background: 0xee_222222, border_thickness: 2, border_color: 0xff_444444, padding: 10) do stack(width: 1.0, max_width: 760, height: 1.0, max_height: 720, v_align: :center, h_align: :center, background: 0xee_222222, border_thickness: 2, border_color: 0xee_222222, padding: 10) do
flow(width: 1.0, height: 0.1, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background Store.application_manager.color(@app_id) background Store.application_manager.color(@app_id)
title @options[:title] || Store.application_manager.name(@app_id) || "Game Settings", fill: true, text_align: :center title @options[:title] || Store.application_manager.name(@app_id) || "Game Settings", fill: true, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, padding: 16, margin_top: 10) do stack(width: 1.0, fill: true, padding: 16, margin_top: 10) do
@@ -27,50 +31,50 @@ class W3DHub
stack(width: 0.5, height: 1.0, margin_right: 8) do stack(width: 0.5, height: 1.0, margin_right: 8) do
tagline "General" tagline "General"
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Default to First Person", fill: true para "Default to First Person", fill: true, text_wrap: :none
toggle_button tip: "Default to First Person", checked: @game_settings.get_value(:default_to_first_person), **BUTTON_STYLE do |btn| toggle_button tip: "Default to First Person", checked: @game_settings.get_value(:default_to_first_person), **BUTTON_STYLE do |btn|
@game_settings.set_value(:default_to_first_person, btn.value) @game_settings.set_value(:default_to_first_person, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Background Downloads", fill: true para "Background Downloads", fill: true, text_wrap: :none
toggle_button tip: "Background Downloads", checked: @game_settings.get_value(:background_downloads), **BUTTON_STYLE do |btn| toggle_button tip: "Background Downloads", checked: @game_settings.get_value(:background_downloads), **BUTTON_STYLE do |btn|
@game_settings.set_value(:background_downloads, btn.value) @game_settings.set_value(:background_downloads, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Enable Hints", fill: true para "Enable Hints", fill: true, text_wrap: :none
toggle_button tip: "Enable Hints", checked: @game_settings.get_value(:hints_enabled), **BUTTON_STYLE do |btn| toggle_button tip: "Enable Hints", checked: @game_settings.get_value(:hints_enabled), **BUTTON_STYLE do |btn|
@game_settings.set_value(:hints_enabled, btn.value) @game_settings.set_value(:hints_enabled, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Enable Chat Log", fill: true para "Enable Chat Log", fill: true, text_wrap: :none
toggle_button tip: "Enable Chat Log", checked: @game_settings.get_value(:chat_log), **BUTTON_STYLE do |btn| toggle_button tip: "Enable Chat Log", checked: @game_settings.get_value(:chat_log), **BUTTON_STYLE do |btn|
@game_settings.set_value(:chat_log, btn.value) @game_settings.set_value(:chat_log, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Show FPS", fill: true para "Show FPS", fill: true, text_wrap: :none
toggle_button tip: "Show FPS", checked: @game_settings.get_value(:show_fps), **BUTTON_STYLE do |btn| toggle_button tip: "Show FPS", checked: @game_settings.get_value(:show_fps), **BUTTON_STYLE do |btn|
@game_settings.set_value(:show_fps, btn.value) @game_settings.set_value(:show_fps, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Show Velocity", fill: true para "Show Velocity", fill: true, text_wrap: :none
toggle_button tip: "Show Velocity", checked: @game_settings.get_value(:show_velocity), **BUTTON_STYLE do |btn| toggle_button tip: "Show Velocity", checked: @game_settings.get_value(:show_velocity), **BUTTON_STYLE do |btn|
@game_settings.set_value(:show_velocity, btn.value) @game_settings.set_value(:show_velocity, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Show Damage Numbers", fill: true para "Show Damage Numbers", fill: true, text_wrap: :none
toggle_button tip: "Show Damage Numbers", checked: @game_settings.get_value(:show_damage_numbers), **BUTTON_STYLE do |btn| toggle_button tip: "Show Damage Numbers", checked: @game_settings.get_value(:show_damage_numbers), **BUTTON_STYLE do |btn|
@game_settings.set_value(:show_damage_numbers, btn.value) @game_settings.set_value(:show_damage_numbers, btn.value)
end end
@@ -80,15 +84,15 @@ class W3DHub
stack(width: 0.5, height: 1.0, margin_left: 8) do stack(width: 0.5, height: 1.0, margin_left: 8) do
tagline "Video" tagline "Video"
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
res_options = @game_settings.get(:resolution_width).options.each_with_index.map do |w, i| res_options = @game_settings.get(:resolution_width).options.each_with_index.map do |w, i|
"#{w[0]}x#{@game_settings.get(:resolution_height).options[i][0]}" "#{w[0]}x#{@game_settings.get(:resolution_height).options[i][0]}"
end end
current_res = "#{@game_settings.get_value(:resolution_width)}x#{@game_settings.get_value(:resolution_height)}" current_res = "#{@game_settings.get_value(:resolution_width)}x#{@game_settings.get_value(:resolution_height)}"
para "Resolution", fill: true para "Resolution", fill: true, text_wrap: :none
list_box items: res_options, choose: current_res, width: 172, **BUTTON_STYLE do |value| list_box items: res_options, choose: current_res, width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
w, h = value.split("x", 2) w, h = value.split("x", 2)
@game_settings.set_value(:resolution_width, w.to_i) @game_settings.set_value(:resolution_width, w.to_i)
@@ -96,23 +100,23 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Windowed Mode", fill: true para "Windowed Mode", fill: true, text_wrap: :none
list_box items: @game_settings.get(:windowed_mode).options.map { |v| v[0] }, choose: @game_settings.get_value(:windowed_mode), width: 172, **BUTTON_STYLE do |value| list_box items: @game_settings.get(:windowed_mode).options.map { |v| v[0] }, choose: @game_settings.get_value(:windowed_mode), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
@game_settings.set_value(:windowed_mode, value) @game_settings.set_value(:windowed_mode, value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Enable VSync", fill: true para "Enable VSync", fill: true, text_wrap: :none
toggle_button tip: "Enable VSync", checked: @game_settings.get_value(:vsync), **BUTTON_STYLE do |btn| toggle_button tip: "Enable VSync", checked: @game_settings.get_value(:vsync), **BUTTON_STYLE do |btn|
@game_settings.set_value(:vsync, btn.value) @game_settings.set_value(:vsync, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Anti-aliasing", fill: true para "Anti-aliasing", fill: true, text_wrap: :none
list_box items: @game_settings.get(:anti_aliasing).options.map { |v| v[0] }, choose: @game_settings.get_value(:anti_aliasing), width: 72, **BUTTON_STYLE do |value| list_box items: @game_settings.get(:anti_aliasing).options.map { |v| v[0] }, choose: @game_settings.get_value(:anti_aliasing), width: 72, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
@game_settings.set_value(:anti_aliasing, value) @game_settings.set_value(:anti_aliasing, value)
end end
end end
@@ -123,8 +127,8 @@ class W3DHub
stack(width: 0.5, height: 1.0, margin_right: 8) do stack(width: 0.5, height: 1.0, margin_right: 8) do
tagline "Audio" tagline "Audio"
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Master Volume", fill: true para "Master Volume", fill: true, text_wrap: :none
slider(height: 1.0, width: 172, value: @game_settings.get_value(:master_volume), margin_right: 8).subscribe(:changed) do |slider| slider(height: 1.0, width: 172, value: @game_settings.get_value(:master_volume), margin_right: 8).subscribe(:changed) do |slider|
@game_settings.set_value(:master_volume, slider.value) @game_settings.set_value(:master_volume, slider.value)
end end
@@ -134,8 +138,8 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Sound Effects", fill: true para "Sound Effects", fill: true, text_wrap: :none
slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_effects_volume), margin_right: 8).subscribe(:changed) do |slider| slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_effects_volume), margin_right: 8).subscribe(:changed) do |slider|
@game_settings.set_value(:sound_effects_volume, slider.value) @game_settings.set_value(:sound_effects_volume, slider.value)
end end
@@ -145,8 +149,8 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Dialogue", fill: true para "Dialogue", fill: true, text_wrap: :none
slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_dialog_volume), margin_right: 8).subscribe(:changed) do |slider| slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_dialog_volume), margin_right: 8).subscribe(:changed) do |slider|
@game_settings.set_value(:sound_dialog_volume, slider.value) @game_settings.set_value(:sound_dialog_volume, slider.value)
end end
@@ -156,8 +160,8 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Music", fill: true para "Music", fill: true, text_wrap: :none
slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_music_volume), margin_right: 8).subscribe(:changed) do |slider| slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_music_volume), margin_right: 8).subscribe(:changed) do |slider|
@game_settings.set_value(:sound_music_volume, slider.value) @game_settings.set_value(:sound_music_volume, slider.value)
end end
@@ -167,8 +171,8 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Cinematic", fill: true para "Cinematic", fill: true, text_wrap: :none
slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_cinematic_volume), margin_right: 8).subscribe(:changed) do |slider| slider(height: 1.0, width: 172, value: @game_settings.get_value(:sound_cinematic_volume), margin_right: 8).subscribe(:changed) do |slider|
@game_settings.set_value(:sound_cinematic_volume, slider.value) @game_settings.set_value(:sound_cinematic_volume, slider.value)
end end
@@ -178,8 +182,8 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Play Sound with Game in Background", fill: true para "Play Sound with Game in Background", fill: true, text_wrap: :none
toggle_button tip: "Play Sound with Game in Background", checked: @game_settings.get_value(:sound_in_background), **BUTTON_STYLE do |btn| toggle_button tip: "Play Sound with Game in Background", checked: @game_settings.get_value(:sound_in_background), **BUTTON_STYLE do |btn|
@game_settings.set_value(:sound_in_background, btn.value) @game_settings.set_value(:sound_in_background, btn.value)
end end
@@ -189,51 +193,51 @@ class W3DHub
stack(width: 0.5, height: 1.0, margin_left: 8) do stack(width: 0.5, height: 1.0, margin_left: 8) do
tagline "Performance" tagline "Performance"
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Texture Detail", fill: true para "Texture Detail", fill: true, text_wrap: :none
list_box items: @game_settings.get(:texture_detail).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_detail), width: 172, **BUTTON_STYLE do |value| list_box items: @game_settings.get(:texture_detail).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_detail), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
@game_settings.set_value(:texture_detail, value) @game_settings.set_value(:texture_detail, value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Texture Filtering", fill: true para "Texture Filtering", fill: true, text_wrap: :none
list_box items: @game_settings.get(:texture_filtering).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_filtering), width: 172, **BUTTON_STYLE do |value| list_box items: @game_settings.get(:texture_filtering).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_filtering), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
@game_settings.set_value(:texture_filtering, value) @game_settings.set_value(:texture_filtering, value)
end end
end end
# flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do # flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
# para "Shader Detail", fill: true # para "Shader Detail", fill: true
# list_box items: @game_settings.get(:texture_filtering).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_filtering), width: 172, **BUTTON_STYLE do |value| # list_box items: @game_settings.get(:texture_filtering).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_filtering), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
# @game_settings.set_value(:texture_filtering, value) # @game_settings.set_value(:texture_filtering, value)
# end # end
# end # end
# flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do # flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
# para "Post Processing Detail", fill: true # para "Post Processing Detail", fill: true
# list_box items: @game_settings.get(:texture_filtering).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_filtering), width: 172, **BUTTON_STYLE do |value| # list_box items: @game_settings.get(:texture_filtering).options.map { |v| v[0] }, choose: @game_settings.get_value(:texture_filtering), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
# @game_settings.set_value(:texture_filtering, value) # @game_settings.set_value(:texture_filtering, value)
# end # end
# end # end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "Shadow Resolution", fill: true para "Shadow Resolution", fill: true, text_wrap: :none
list_box items: @game_settings.get(:shadow_resolution).options.map { |v| v[0] }, choose: @game_settings.get_value(:shadow_resolution), width: 172, **BUTTON_STYLE do |value| list_box items: @game_settings.get(:shadow_resolution).options.map { |v| v[0] }, choose: @game_settings.get_value(:shadow_resolution), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
@game_settings.set_value(:shadow_resolution, value) @game_settings.set_value(:shadow_resolution, value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "High Quality Shadows", fill: true para "High Quality Shadows", fill: true, text_wrap: :none
toggle_button tip: "High Quality Shadows", checked: @game_settings.get_value(:background_downloads), **BUTTON_STYLE do |btn| toggle_button tip: "High Quality Shadows", checked: @game_settings.get_value(:background_downloads), **BUTTON_STYLE do |btn|
@game_settings.set_value(:background_downloads, btn.value) @game_settings.set_value(:background_downloads, btn.value)
end end
end end
flow(width: 1.0, height: 24, margin: 4, margin_left: 10) do flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
para "FPS Limit", fill: true para "FPS Limit", fill: true, text_wrap: :none
list_box items: @game_settings.get(:fps).options.map { |v| v[0] }, choose: @game_settings.get_value(:fps), width: 172, **BUTTON_STYLE do |value| list_box items: @game_settings.get(:fps).options.map { |v| v[0] }, choose: @game_settings.get_value(:fps), width: 172, theme: LIST_ITEM_THEME, **BUTTON_STYLE do |value|
@game_settings.set_value(:fps, value.to_i) @game_settings.set_value(:fps, value.to_i)
end end
end end
@@ -241,7 +245,7 @@ class W3DHub
end end
end end
flow(width: 1.0, height: 0.1, padding: 8) do flow(width: 1.0, height: 46, padding: 8) do
button "Cancel", width: 0.25 do button "Cancel", width: 0.25 do
pop_state pop_state
@options[:cancel_callback]&.call @options[:cancel_callback]&.call

View File

@@ -0,0 +1,85 @@
class W3DHub
class States
class ImportGameDialog < CyberarmEngine::GuiState
def setup
@application = Store.applications.games.find { |g| g.id == @options[:app_id] }
@channel = @application.channels.find { |c| c.id == @options[:channel] }
theme W3DHub::THEME
background 0x88_525252
stack(width: 1.0, max_width: 760, height: 1.0, max_height: 268, v_align: :center, h_align: :center, background: 0xee_222222) do
# Title bar
flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000
# image "#{GAME_ROOT_PATH}/media/ui_icons/export.png", width: 32, align: :center, color: 0xaa_ffffff
# tagline "<b>#{I18n.t(:"server_browser.direct_connect")}</b>", fill: true, text_align: :center
title "Import #{@application.name} (#{@channel.name})", width: 1.0, text_align: :center, font: BOLD_FONT
end
stack(width: 1.0, fill: true, padding_left: 8, padding_right: 8) do
stack(width: 1.0, height: 72) do
para "Path to Executable:"
flow(width: 1.0, fill: true) do
@game_path = edit_line "", fill: true, height: 1.0
button "Browse...", width: 128, height: 1.0, tip: "Browse for game executable" do
path = W3DHub.ask_file
@game_path.value = path if !path.empty? && File.exist?(path)
end
end
end
flow(fill: true)
flow(width: 1.0, margin_top: 8, height: 46, padding_bottom: 8) do
button "Cancel", fill: true, margin_right: 4 do
pop_state
end
flow(fill: true)
@save_button = button "Save", fill: true, margin_left: 4, enabled: false do
pop_state
Store.application_manager.imported!(@application, @channel, @game_path.value)
end
end
end
end
end
def draw
previous_state&.draw
Gosu.flush
super
end
def update
super
@save_button.enabled = valid?
end
def button_down(id)
super
case id
when Gosu::KB_ESCAPE
pop_state
end
end
def valid?
path = @game_path.value
File.exist?(path) && !File.directory?(path) && File.extname(path) == ".exe"
end
end
end
end

View File

@@ -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

View File

@@ -6,24 +6,25 @@ class W3DHub
theme(W3DHub::THEME) theme(W3DHub::THEME)
background 0xee_444444 background 0xaa_525252
stack(width: 1.0, max_width: 720, height: 1.0, max_height: 480, v_align: :center, h_align: :center, background: 0xee_222222) do stack(width: 1.0, max_width: 720, height: 1.0, max_height: 480, v_align: :center, h_align: :center, background: 0xee_222222) do
flow(width: 1.0, height: 32, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000 background 0x88_000000
image "#{GAME_ROOT_PATH}/media/ui_icons/warning.png", width: 32, align: :center, color: 0xff_ff8800 image "#{GAME_ROOT_PATH}/media/ui_icons/warning.png", height: 1.0, align: :center, color: 0xff_ff8800
tagline "<b>#{@options[:title]}</b>", width: 0.9, text_align: :center title "<b>#{@options[:title]}</b>", width: 0.9, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, padding: 16) do stack(width: 1.0, fill: true, padding: 16) do
para @options[:message], width: 1.0 para @options[:message], width: 1.0
end end
stack(width: 1.0, height: 40, padding: 8) do stack(width: 1.0, height: 46, padding: 8) do
button "Okay", width: 1.0 do button "Okay", width: 1.0 do
pop_state pop_state
@options[:accept_callback]&.call
end end
end end
end end

View File

@@ -6,15 +6,15 @@ class W3DHub
theme(W3DHub::THEME) theme(W3DHub::THEME)
background 0xee_444444 background 0xaa_525252
stack(width: 1.0, max_width: 720, height: 1.0, max_height: 256, v_align: :center, h_align: :center, background: 0xee_222222) do stack(width: 1.0, max_width: 720, height: 1.0, max_height: 256, v_align: :center, h_align: :center, background: 0xee_222222) do
flow(width: 1.0, height: 32, padding: 8) do flow(width: 1.0, height: 36, padding: 8) do
background 0x88_000000 background 0x88_000000
image "#{GAME_ROOT_PATH}/media/ui_icons/question.png", width: 32, align: :center, color: 0xff_ff8800 image "#{GAME_ROOT_PATH}/media/ui_icons/question.png", height: 1.0, align: :center, color: 0xff_ff8800
tagline "<b>#{@options[:title]}</b>", fill: true, text_align: :center title "<b>#{@options[:title]}</b>", fill: true, text_align: :center, font: BOLD_FONT
end end
stack(width: 1.0, fill: true, padding: 16) do stack(width: 1.0, fill: true, padding: 16) do
@@ -22,7 +22,7 @@ class W3DHub
@prompt_entry = edit_line @options[:prefill].to_s, margin_top: 24, width: 1.0, autofocus: true, focus: true, type: @options[:input_type] == :password ? :password : :text @prompt_entry = edit_line @options[:prefill].to_s, margin_top: 24, width: 1.0, autofocus: true, focus: true, type: @options[:input_type] == :password ? :password : :text
end end
flow(width: 1.0, height: 40, padding: 8) do flow(width: 1.0, height: 46, padding: 8) do
button "Cancel", width: 0.25 do button "Cancel", width: 0.25 do
pop_state pop_state
@options[:cancel_callback]&.call(@prompt_entry.value) @options[:cancel_callback]&.call(@prompt_entry.value)

View File

@@ -1,6 +1,11 @@
class W3DHub class W3DHub
class States class States
class Interface < CyberarmEngine::GuiState class Interface < CyberarmEngine::GuiState
APPLICATIONS_UPDATE_INTERVAL = 10 * 60 * 1000 # ten minutes
SERVER_LIST_UPDATE_INTERVAL = 5 * 60 * 1000 # five minutes
DEFAULT_BACKGROUND_IMAGE = "#{GAME_ROOT_PATH}/media/banners/background.png".freeze
attr_accessor :interface_task_update_pending attr_accessor :interface_task_update_pending
@@instance = nil @@instance = nil
@@ -18,6 +23,9 @@ class W3DHub
@service_status = @options[:service_status] @service_status = @options[:service_status]
@applications = @options[:applications] @applications = @options[:applications]
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # 5 minutes
@interface_task_update_pending = nil @interface_task_update_pending = nil
@page = nil @page = nil
@@ -27,10 +35,12 @@ class W3DHub
theme(W3DHub::THEME) theme(W3DHub::THEME)
@interface_container = stack(width: 1.0, height: 1.0, border_thickness: 1, border_color: W3DHub::BORDER_COLOR, background_image: "#{GAME_ROOT_PATH}/media/banners/background.png", background_image_color: 0xff_525252, background_image_mode: :fill) do @interface_container = stack(width: 1.0, height: 1.0, border_thickness: 1, border_color: W3DHub::BORDER_COLOR, background_image: DEFAULT_BACKGROUND_IMAGE, background_image_mode: :fill) do
background 0xff_252525 background 0xff_252525
@header_container = flow(width: 1.0, height: 84, padding: 4, border_thickness_bottom: 1, border_color_bottom: W3DHub::BORDER_COLOR) do @header_container = flow(width: 1.0, height: 84, padding: 4, border_thickness_bottom: 1, border_color_bottom: W3DHub::BORDER_COLOR) do
background 0xaa_151515
flow(width: 148, height: 1.0) do flow(width: 148, height: 1.0) do
flow(fill: true) flow(fill: true)
image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 84 image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 84
@@ -38,36 +48,48 @@ class W3DHub
end end
@navigation_container = stack(fill: true, height: 1.0) do @navigation_container = stack(fill: true, height: 1.0) do
flow(width: 1.0, fill: true) do @nav_padding_top_container = flow(fill: true)
flow(width: 1.0, height: 36) do
# background 0xff_666666 # background 0xff_666666
link I18n.t(:"interface.games").upcase, text_size: 34 do link I18n.t(:"interface.games").upcase, text_size: 34, font: BOLD_FONT do
page(W3DHub::Pages::Games) page(W3DHub::Pages::Games)
end end
link I18n.t(:"interface.servers").upcase, text_size: 34, 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.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, 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.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, 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.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, 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.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
page(W3DHub::Pages::Settings) page(W3DHub::Pages::Settings)
end end
end end
@nav_padding_bottom_container = flow(fill: true)
# Installer task display # Installer task display
flow(width: 1.0, height: 0.5) do @application_taskbar_container = flow(width: 1.0, height: 0.5) do
@application_taskbar_container = stack(width: 1.0, height: 1.0, margin_left: 16, margin_right: 16) do stack(width: 1.0, height: 1.0, margin_left: 16, margin_right: 16) do
flow(width: 1.0, height: 0.65) do flow(width: 1.0, height: 0.65) do
@application_taskbar_label = inscription "", width: 0.60, text_wrap: :none @application_taskbar_label = para "", fill: true, text_wrap: :none
@application_taskbar_status_label = inscription "", width: 0.40, text_align: :right, text_wrap: :none @application_taskbar_status_label = para "", width: 0.4, min_width: 256, text_align: :right, text_wrap: :none
end end
@application_taskbar_progressbar = progress fraction: 0.0, height: 2, width: 1.0 @application_taskbar_progressbar = progress fraction: 0.0, height: 2, width: 1.0
@@ -89,8 +111,8 @@ class W3DHub
tagline "<b>#{I18n.t(:"interface.not_logged_in")}</b>", text_wrap: :none tagline "<b>#{I18n.t(:"interface.not_logged_in")}</b>", text_wrap: :none
flow(width: 1.0) do flow(width: 1.0) do
link(I18n.t(:"interface.log_in"), text_size: 16, width: 0.5) { page(W3DHub::Pages::Login) } link(I18n.t(:"interface.log_in"), text_size: 22, width: 0.5) { page(W3DHub::Pages::Login) }
link I18n.t(:"interface.register"), text_size: 16, width: 0.49 do link I18n.t(:"interface.register"), text_size: 22, width: 0.49 do
W3DHub.url("https://secure.w3dhub.com/forum/index.php?app=core&module=global&section=register") W3DHub.url("https://secure.w3dhub.com/forum/index.php?app=core&module=global&section=register")
end end
end end
@@ -124,6 +146,36 @@ 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)
@@ -159,10 +211,10 @@ class W3DHub
@page @page
end end
def update_server_browser(server) def update_server_browser(server, mode = :update)
return unless @page.is_a?(Pages::ServerBrowser) return unless @page.is_a?(Pages::ServerBrowser)
@page.refresh_server_list(server) @page.refresh_server_list(server, mode)
end end
def update_server_ping(server) def update_server_ping(server)
@@ -172,11 +224,15 @@ class W3DHub
end end
def show_application_taskbar def show_application_taskbar
@nav_padding_top_container.hide
@nav_padding_bottom_container.hide
@application_taskbar_container.show @application_taskbar_container.show
end end
def hide_application_taskbar def hide_application_taskbar
@application_taskbar_container.hide @application_taskbar_container.hide
@nav_padding_top_container.show
@nav_padding_bottom_container.show
end end
def update_interface_task_status(task) def update_interface_task_status(task)

View File

@@ -5,14 +5,13 @@ class W3DHub
window.show_cursor = true window.show_cursor = true
theme(W3DHub::THEME) theme(W3DHub::THEME)
background 0x88_252525
flow(width: 1.0, height: 1.0) 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: 128, padding: 16) do
background 0xff_252525 background 0xaa_353535
end end
flow(fill: true) flow(fill: true)
@@ -30,7 +29,7 @@ class W3DHub
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
end end
flow(width: 1.0, height: 40) do flow(width: 1.0, height: 46) do
stack(fill: true, height: 1.0) do stack(fill: true, height: 1.0) do
link "Skip", border_color_bottom: 0xff_777777 do link "Skip", border_color_bottom: 0xff_777777 do
pop_state pop_state
@@ -55,7 +54,7 @@ class W3DHub
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 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
end end
flow(width: 1.0, height: 40) do flow(width: 1.0, height: 46) do
flow(fill: true, height: 1.0) do flow(fill: true, height: 1.0) do
button "< Back" do button "< Back" do
@card_container.clear { card_welcome } @card_container.clear { card_welcome }
@@ -88,7 +87,7 @@ class W3DHub
caption "Subscribe to our YouTube channel", margin_left: 32 caption "Subscribe to our YouTube channel", margin_left: 32
end end
flow(width: 1.0, height: 40) do flow(width: 1.0, height: 46) do
flow(fill: true, height: 1.0) do flow(fill: true, height: 1.0) do
button "< Back" do button "< Back" do
@card_container.clear { card_getting_started } @card_container.clear { card_getting_started }

View File

@@ -7,12 +7,28 @@ class W3DHub
MAX_PAGE_WIDTH = 1200 MAX_PAGE_WIDTH = 1200
TESTING_BUTTON = { TESTING_BUTTON = {
background: 0xff_ff8800, background: 0xff_ff8800..0xff_dd6600,
border_color: Gosu::Color::NONE,
hover: { hover: {
background: 0xff_ffaa00 background: 0xff_dd6600..0xff_bb4400,
border_color: 0xff_ff8800,
}, },
active: { active: {
background: 0xff_ffec00 background: 0xff_bb4400..0xff_dd6600,
border_color: 0xff_ff8800
}
}
DANGEROUS_BUTTON = {
background: 0xff_800000..0xff_600000,
border_color: Gosu::Color::NONE,
hover: {
background: 0xff_600000..0xff_400000,
border_color: 0xff_800000,
},
active: {
background: 0xff_400000..0xff_600000,
border_color: 0xff_800000
} }
} }
@@ -20,46 +36,93 @@ class W3DHub
THEME = { THEME = {
ToolTip: { ToolTip: {
background: 0xff_dedede, background: 0xff_222222,
color: 0xaa_000000, color: 0xff_f2f2f2,
text_size: 18, text_size: 22,
text_static: true,
text_border: false, text_border: false,
text_shadow: false text_shadow: false
}, },
TextBlock: { TextBlock: {
font: BOLD_FONT, font: REGULAR_FONT,
color: 0xff_f2f2f2,
text_static: true,
text_border: false, text_border: false,
text_shadow: true, text_shadow: true,
text_shadow_size: 1, text_shadow_size: 1,
text_shadow_color: 0x88_000000 text_shadow_color: 0x88_000000
}, },
EditLine: { Banner: { # < TextBlock
border_thickness: 2, text_size: 48,
border_color: Gosu::Color::WHITE, font: BOLD_FONT
hover: { color: Gosu::Color::WHITE } },
Title: { # < TextBlock
text_size: 34,
font: BOLD_FONT
},
Subtitle: { # < TextBlock
text_size: 28,
font: BOLD_FONT
},
Tagline: { # < TextBlock
text_size: 26,
font: BOLD_FONT
},
Caption: { # < TextBlock
text_size: 24
},
Para: { # < TextBlock
text_size: 22
},
Inscription: { # < TextBlock
text_size: 18
}, },
Link: { Link: {
color: 0xff_cdcdcd, color: 0xff_cdcdcd,
hover: { hover: {
color: Gosu::Color::WHITE color: 0xff_f2f2f2
}, },
active: { active: {
color: 0xff_eeeeee color: 0xff_eeeeee
} }
}, },
Button: { Button: {
text_size: 18, font: BOLD_FONT,
color: 0xff_f2f2f2,
text_size: 22,
padding_top: 8, padding_top: 8,
padding_left: 16, padding_left: 16,
padding_right: 16, padding_right: 16,
padding_bottom: 8, padding_bottom: 8,
border_thickness: 2,
border_color: Gosu::Color::NONE, border_color: Gosu::Color::NONE,
background: 0xff_00acff, background: 0xff_0074e0..0xff_0052c0,
hover: { hover: {
background: 0xff_bee6fd color: 0xff_f2f2f2,
background: 0xff_0052c0..0xff_0030a0,
border_color: 0xff_0074e0
}, },
active: { active: {
background: 0xff_add5ec color: 0xff_aaaaaa,
background: 0xff_0030a0..0xff_0052c0,
border_color: 0xff_0074e0
}
},
EditLine: {
font: REGULAR_FONT,
color: 0xff_f2f2f2,
background: 0xff_383838,
border_thickness: 2,
border_color: 0xff_0074e0,
hover: {
color: 0xff_f2f2f2,
background: 0xff_323232,
border_color: 0xff_0074e0
},
active: {
color: 0xff_f2f2f2,
background: 0xff_4b4b4b,
border_color: 0xff_0074e0
} }
}, },
ToggleButton: { ToggleButton: {
@@ -70,7 +133,8 @@ class W3DHub
checkmark_image: "#{GAME_ROOT_PATH}/media/ui_icons/checkmark.png" checkmark_image: "#{GAME_ROOT_PATH}/media/ui_icons/checkmark.png"
}, },
Progress: { Progress: {
fraction_background: 0xff_00acff, background: 0xff_353535,
fraction_background: 0xff_0074e0,
border_thickness: 0 border_thickness: 0
}, },
ListBox: { ListBox: {
@@ -78,21 +142,42 @@ class W3DHub
padding_right: 8 padding_right: 8
}, },
Slider: { Slider: {
border_color: 0xff_00acff border_color: 0xff_0074e0
}, },
Handle: { Handle: {
text_size: 18, text_size: 22,
padding_top: 8, padding_top: 8,
padding_left: 2, padding_left: 2,
padding_right: 2, padding_right: 2,
padding_bottom: 8, padding_bottom: 8,
border_color: Gosu::Color::NONE, border_color: Gosu::Color::NONE,
background: 0xff_00acff, background: 0xff_0074e0,
hover: { hover: {
background: 0xff_bee6fd background: 0xff_004c94
}, },
active: { active: {
background: 0xff_add5ec background: 0xff_005aad
}
},
Menu: {
width: 200,
border_color: 0xaa_efefef,
border_thickness: 1
},
MenuItem: {
width: 1.0,
text_left: :left,
margin: 0,
border_color: Gosu::Color::NONE,
background: 0xff_0074e0,
hover: {
color: 0xff_f2f2f2,
background: 0xff_0052c0,
border_color: Gosu::Color::NONE
},
active: {
background: 0xff_0030a0,
border_color: Gosu::Color::NONE
} }
} }
} }

View File

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

View File

@@ -1,6 +1,7 @@
class W3DHub class W3DHub
class Window < CyberarmEngine::Window class Window < CyberarmEngine::Window
def setup def setup
self.show_stats_plotter = false
self.caption = I18n.t(:app_name) self.caption = I18n.t(:app_name)
Store[:server_list] = [] Store[:server_list] = []
@@ -9,11 +10,6 @@ class W3DHub
Store[:main_thread_queue] = [] Store[:main_thread_queue] = []
# Repair/Upgrade schema
Store.settings[:favorites] ||= {}
Store.settings.save_settings
begin begin
I18n.locale = Store.settings[:language] I18n.locale = Store.settings[:language]
rescue I18n::InvalidLocale rescue I18n::InvalidLocale

Binary file not shown.

Before

Width:  |  Height:  |  Size: 771 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 528 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

View File

@@ -14,16 +14,25 @@ require "logger"
require "time" require "time"
require "base64" require "base64"
require "zip" require "zip"
require "excon"
class W3DHub class W3DHub
W3DHUB_DEBUG = ARGV.join.include?("--debug") W3DHUB_DEBUG = ARGV.join.include?("--debug")
W3DHUB_DEVELOPER = ARGV.join.include?("--developer") W3DHUB_DEVELOPER = ARGV.join.include?("--developer")
GAME_ROOT_PATH = File.expand_path(".", __dir__) # Use the real working directory as the root for runtime data/logs
CACHE_PATH = "#{GAME_ROOT_PATH}/data/cache" GAME_ROOT_PATH = Dir.pwd
SETTINGS_FILE_PATH = "#{GAME_ROOT_PATH}/data/settings.json"
LOGGER = Logger.new("#{GAME_ROOT_PATH}/data/logs/w3d_hub_linux_launcher.log", "daily") CACHE_PATH = "#{GAME_ROOT_PATH}/data/cache"
LOGS_PATH = "#{GAME_ROOT_PATH}/data/logs"
SETTINGS_FILE_PATH = "#{GAME_ROOT_PATH}/data/settings.json"
APPLICATIONS_CACHE_FILE_PATH = "#{GAME_ROOT_PATH}/data/applications_cache.json"
# Ensure data/cache and data/logs exist
FileUtils.mkdir_p(CACHE_PATH) unless Dir.exist?(CACHE_PATH)
FileUtils.mkdir_p(LOGS_PATH) unless Dir.exist?(LOGS_PATH)
LOGGER = Logger.new("#{LOGS_PATH}/w3d_hub_linux_launcher.log", "daily")
LOGGER.level = Logger::Severity::DEBUG # W3DHUB_DEBUG ? Logger::Severity::DEBUG : Logger::Severity::WARN LOGGER.level = Logger::Severity::DEBUG # W3DHUB_DEBUG ? Logger::Severity::DEBUG : Logger::Severity::WARN
LOG_TAG = "W3DHubLinuxLauncher" LOG_TAG = "W3DHubLinuxLauncher"
@@ -84,8 +93,6 @@ I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"]
I18n.default_locale = :en I18n.default_locale = :en
# GUI_DEBUG = true # GUI_DEBUG = true
require_relative "lib/gui_state_ext"
require_relative "lib/win32_stub" unless Gem.win_platform? require_relative "lib/win32_stub" unless Gem.win_platform?
require_relative "lib/version" require_relative "lib/version"
@@ -110,7 +117,6 @@ require_relative "lib/application_manager/tasks/installer"
require_relative "lib/application_manager/tasks/updater" require_relative "lib/application_manager/tasks/updater"
require_relative "lib/application_manager/tasks/uninstaller" require_relative "lib/application_manager/tasks/uninstaller"
require_relative "lib/application_manager/tasks/repairer" require_relative "lib/application_manager/tasks/repairer"
require_relative "lib/application_manager/tasks/importer"
require_relative "lib/states/demo_input_delay" require_relative "lib/states/demo_input_delay"
require_relative "lib/states/boot" require_relative "lib/states/boot"
require_relative "lib/states/interface" require_relative "lib/states/interface"
@@ -121,6 +127,8 @@ require_relative "lib/states/dialogs/prompt_dialog"
require_relative "lib/states/dialogs/confirm_dialog" require_relative "lib/states/dialogs/confirm_dialog"
require_relative "lib/states/dialogs/direct_connect_dialog" require_relative "lib/states/dialogs/direct_connect_dialog"
require_relative "lib/states/dialogs/game_settings_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"
require_relative "lib/api/service_status" require_relative "lib/api/service_status"
@@ -150,7 +158,18 @@ require_relative "lib/asterisk/states/game_form"
require_relative "lib/asterisk/states/irc_profile_form" require_relative "lib/asterisk/states/irc_profile_form"
require_relative "lib/asterisk/states/server_profile_form" require_relative "lib/asterisk/states/server_profile_form"
require "win32/process" if W3DHub.windows? if W3DHub.windows?
require "libui"
require "win32/process"
# Using a WHOLE ui library for: native file/folder open dialogs...
LibUI.init
LIBUI_WINDOW = LibUI.new_window("", 100, 100, 0)
at_exit do
LibUI.control_destroy(LIBUI_WINDOW)
LibUI.uninit
end
end
logger.info(W3DHub::LOG_TAG) { "W3D Hub Linux Launcher v#{W3DHub::VERSION}" } logger.info(W3DHub::LOG_TAG) { "W3D Hub Linux Launcher v#{W3DHub::VERSION}" }