Compare commits
101 Commits
c2528f7e12
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 603328a51f | |||
| 48297ad9cd | |||
| 39fbb9df38 | |||
| bc9a524a55 | |||
| d92a8753d8 | |||
| b299593076 | |||
| ce10cdc658 | |||
| 5a3f350015 | |||
| d53299e904 | |||
| d12d3ff6b8 | |||
| d67ffa14a3 | |||
| 71047ce9e8 | |||
| 7da716dde4 | |||
| 3a72a2e094 | |||
| 3c565e6fee | |||
| 2dc750a686 | |||
| ed119a4925 | |||
| e4d99aac00 | |||
| e9b8638c27 | |||
| 4997cfabb0 | |||
|
|
0c906464f0 | ||
|
|
5bafc77d97 | ||
| 30aa44312d | |||
| 2031f589b7 | |||
| b909952790 | |||
| 6d651c7ad6 | |||
| 60909b0963 | |||
| 48617b26da | |||
| ad2544a56b | |||
| 80c104772f | |||
| 09082c0c5d | |||
|
|
27e5da9fd2 | ||
| 0bb8ef5f19 | |||
| cc0910e68e | |||
| fd728fa945 | |||
| ec6dfe8371 | |||
| 49d501a8b0 | |||
|
|
e239f9cd4d | ||
|
|
b68d24deda | ||
|
|
1081832df0 | ||
|
|
c3cee78265 | ||
|
|
4d3163740a | ||
|
|
f1953c45e7 | ||
| 685a1aa82c | |||
| 9dfee9d1d3 | |||
| 1e0adc398c | |||
| 3485d5b61a | |||
| cb81a51bfe | |||
| 314201f238 | |||
|
|
12721cbfbc | ||
|
|
5ef11fbee8 | ||
| e73abce65e | |||
| 9b1cb1bb95 | |||
| c9185e9859 | |||
| e4a0d2a848 | |||
| 1401b80057 | |||
| cfae4ec3a5 | |||
| c344e6a522 | |||
| 696c30aa63 | |||
| 1818d8bec9 | |||
| 4af10a998e | |||
| 6736abc277 | |||
| c9c5e18d70 | |||
| 67c52c84a1 | |||
| 80d1fa865c | |||
| a1810e3f2c | |||
| 75b9e3e14a | |||
| 7fdb406588 | |||
| 0ab616f48b | |||
| e035b1ed58 | |||
| 3f7ec2fb5c | |||
| 6d209c8942 | |||
| f55924596d | |||
| d84c8321c5 | |||
| 38e0de76df | |||
| 9bdca9eba1 | |||
| 02307f1789 | |||
| c1ca3ec80e | |||
| 29c8667602 | |||
| b594cdae96 | |||
| d350e51d0b | |||
| 0cbe013a11 | |||
| 5c806852a5 | |||
| 655fc14557 | |||
| 6772d4757f | |||
| 3383cbd019 | |||
| aceed86cb4 | |||
| 58daeffb14 | |||
| 2a4bb87e68 | |||
| fc643235b5 | |||
| cd0db4e0fc | |||
| 84051103fc | |||
| f2dd844181 | |||
| a512669a2d | |||
| 85d408fad7 | |||
| d4e81dd441 | |||
| 0d1333ee4f | |||
| 458a9e8832 | |||
| 924f4c2b75 | |||
| 0b9b519848 | |||
| f9d401e713 |
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: # Replace with a single Open Collective username
|
||||||
|
ko_fi: cyberarm
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
47
.github/workflows/build-tebako.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build Launcher Binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
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
|
||||||
22
Gemfile
@@ -1,18 +1,26 @@
|
|||||||
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 "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
|
||||||
|
|||||||
57
Gemfile.lock
@@ -1,32 +1,45 @@
|
|||||||
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)
|
cri (2.15.12)
|
||||||
excon (~> 0.88)
|
cyberarm_engine (0.24.5)
|
||||||
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.2)
|
||||||
ffi (1.16.3)
|
logger
|
||||||
ffi-win32-extensions (1.0.4)
|
ffi (1.17.0)
|
||||||
ffi
|
ffi-win32-extensions (1.1.0)
|
||||||
|
ffi (>= 1.15.5, <= 1.17.0)
|
||||||
|
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)
|
||||||
|
ocran (1.3.17)
|
||||||
|
fiddle (~> 1.0)
|
||||||
|
rake (13.3.1)
|
||||||
|
releasy (0.2.4)
|
||||||
|
bundler (>= 1.2.1)
|
||||||
|
cri (~> 2.15.0)
|
||||||
|
ocran (~> 1.3.0)
|
||||||
|
rake (>= 0.9.2.2)
|
||||||
|
rexml (3.4.4)
|
||||||
|
rubyzip (3.2.2)
|
||||||
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 +49,19 @@ GEM
|
|||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x64-mingw-ucrt
|
x64-mingw-ucrt
|
||||||
x64-mingw32
|
|
||||||
x86_64-linux
|
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
base64
|
base64
|
||||||
|
bundler (~> 2.4.3)
|
||||||
cyberarm_engine
|
cyberarm_engine
|
||||||
digest-crc
|
digest-crc
|
||||||
|
excon
|
||||||
i18n
|
i18n
|
||||||
ircparser
|
ircparser
|
||||||
|
libui
|
||||||
|
ocran
|
||||||
|
rake
|
||||||
|
releasy
|
||||||
rexml
|
rexml
|
||||||
rubyzip
|
rubyzip
|
||||||
sdl2-bindings
|
sdl2-bindings
|
||||||
@@ -53,4 +70,4 @@ DEPENDENCIES
|
|||||||
win32-security
|
win32-security
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.3
|
2.4.22
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ It runs natively on Linux! No mucking about trying to get .NET 4.6.1 or somethin
|
|||||||
Only requires OpenGL, Ruby, and a few gems.
|
Only requires OpenGL, Ruby, and a few gems.
|
||||||
|
|
||||||
## Installing
|
## Installing
|
||||||
* Install Ruby 3.0+, from your package manager.
|
* Install Ruby 3.4+, from your package manager.
|
||||||
* Install Gosu's [dependencies](https://github.com/gosu/gosu/wiki/Getting-Started-on-Linux).
|
* Install Gosu's [dependencies](https://github.com/gosu/gosu/wiki/Getting-Started-on-Linux).
|
||||||
* Install required gems: `bundle install`
|
* Install required gems: `bundle install`
|
||||||
|
|
||||||
|
|||||||
6
Rakefile
@@ -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
|
||||||
235
lib/api.rb
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
49
lib/cache.rb
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
102
lib/i18n.rb
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# The I18n gem is a real pain to work with when packaging with Ocra(n)
|
||||||
|
# and we're not using its 'advanced' features so emulate its API here.
|
||||||
|
|
||||||
|
require "yaml"
|
||||||
|
|
||||||
|
class I18n
|
||||||
|
class InvalidLocale < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
@locale = :en
|
||||||
|
@default_locale = :en
|
||||||
|
@load_path = []
|
||||||
|
|
||||||
|
@translations = {}
|
||||||
|
|
||||||
|
def self.load_path
|
||||||
|
@load_path
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_locale
|
||||||
|
@default_locale.to_sym
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_locale=(locale)
|
||||||
|
@default_locale = locale.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.locale
|
||||||
|
@locale.to_sym
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.locale=(locale)
|
||||||
|
locale = locale.to_s
|
||||||
|
|
||||||
|
raise InvalidLocale unless valid_locale?(locale)
|
||||||
|
|
||||||
|
@locale = locale
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.t(symbol)
|
||||||
|
return symbol.to_s unless valid_locale?(@locale)
|
||||||
|
|
||||||
|
@translations[@locale] || load_locale(@locale)
|
||||||
|
|
||||||
|
translations = @translations[@locale]
|
||||||
|
return translations[symbol] if translations
|
||||||
|
|
||||||
|
translation = @translations.dig(@default_locale, symbol)
|
||||||
|
return translation if translation
|
||||||
|
|
||||||
|
return symbol.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.available_locales
|
||||||
|
@load_path.flatten.map { |f| File.basename(f, ".yml").to_s.downcase.to_sym }
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def self.load_locale(locale)
|
||||||
|
locale = locale.to_s
|
||||||
|
|
||||||
|
return if @translations[locale] && !@translations[locale].empty?
|
||||||
|
|
||||||
|
if (file = valid_locale?(locale))
|
||||||
|
yaml = YAML.load_file(file)
|
||||||
|
|
||||||
|
raise InvalidLocale unless yaml[locale]
|
||||||
|
|
||||||
|
key = ""
|
||||||
|
hash = yaml[locale]
|
||||||
|
hash.each_pair do |key, v|
|
||||||
|
if v.is_a?(String)
|
||||||
|
@translations[locale] ||= {}
|
||||||
|
@translations[locale][key.to_sym] = v
|
||||||
|
else
|
||||||
|
load_locale_part(locale, key, v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load_locale_part(locale, key, part)
|
||||||
|
locale = locale.to_s
|
||||||
|
|
||||||
|
part.each_pair do |k, v|
|
||||||
|
if v.is_a?(String)
|
||||||
|
@translations[locale] ||= {}
|
||||||
|
@translations[locale]["#{key}.#{k}".to_sym] = v
|
||||||
|
else
|
||||||
|
load_locale_part(locale, "#{key}.#{k}", v)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.valid_locale?(locale)
|
||||||
|
locale = locale.to_s
|
||||||
|
|
||||||
|
@load_path.flatten.find do |file|
|
||||||
|
File.basename(file, ".yml").to_s.downcase.strip == locale
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
24
lib/mixer.rb
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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§ion=register")
|
W3DHub.url("https://secure.w3dhub.com/forum/index.php?app=core&module=global§ion=register")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -3,82 +3,64 @@ 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?
|
flow(width: 256, height: 64, h_align: :center, margin_top: 16) 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
|
end
|
||||||
|
|
||||||
flow(width: 1.0, height: 0.13, margin_top: 16) do
|
flow(fill: true)
|
||||||
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
|
end
|
||||||
|
button("Clear package cache: #{W3DHub.format_size(Dir.glob("#{Store.settings[:package_cache_dir]}/**/**").map { |f| File.file?(f) ? File.size(f) : 0}.sum)}", **DANGEROUS_BUTTON) do
|
||||||
button "Save" do
|
# TODO.
|
||||||
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
|
end
|
||||||
@@ -108,7 +90,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
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class W3DHub
|
|||||||
class Settings
|
class Settings
|
||||||
def self.defaults
|
def self.defaults
|
||||||
{
|
{
|
||||||
language: Gosu.user_languages.first.split("_").first,
|
language: Gosu.user_languages.first&.split("_")&.first || "en",
|
||||||
app_install_dir: default_app_install_dir,
|
app_install_dir: default_app_install_dir,
|
||||||
package_cache_dir: default_package_cache_dir,
|
package_cache_dir: default_package_cache_dir,
|
||||||
parallel_downloads: 4,
|
parallel_downloads: 4,
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
85
lib/states/dialogs/import_game_dialog.rb
Normal 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
|
||||||
62
lib/states/dialogs/launcher_updater_dialog.rb
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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§ion=register")
|
W3DHub.url("https://secure.w3dhub.com/forum/index.php?app=core&module=global§ion=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)
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
129
lib/theme.rb
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 771 KiB |
|
Before Width: | Height: | Size: 528 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 225 KiB |
|
Before Width: | Height: | Size: 714 KiB |
|
Before Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 240 KiB |
@@ -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"
|
||||||
@@ -75,17 +84,15 @@ class W3DHub
|
|||||||
BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff")
|
BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff")
|
||||||
end
|
end
|
||||||
|
|
||||||
require "i18n"
|
|
||||||
require "websocket-client-simple"
|
require "websocket-client-simple"
|
||||||
require "English"
|
require "English"
|
||||||
require "sdl2"
|
require "sdl2"
|
||||||
|
|
||||||
|
require_relative "lib/i18n"
|
||||||
I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"]
|
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}" }
|
||||||
|
|
||||||
|
|||||||