Compare commits
128 Commits
cd0db4e0fc
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| a856dd5dd4 | |||
| 38a882179c | |||
| f350a9a937 | |||
| 2d429cb834 | |||
| d72142a409 | |||
| 9cb41a8693 | |||
| f024109327 | |||
| 287022f2b8 | |||
| 68df923bea | |||
| ddbec8d72c | |||
| 70d4e0c40f | |||
| f30658ffc2 | |||
| 9e8f4e1c71 | |||
| b7e2e69af9 | |||
| 3dbfd23b10 | |||
| d1d667056b | |||
| c881296ac8 | |||
| d630e5044e | |||
| 632fc2c05c | |||
| bf8f440ec7 | |||
| 633aa10d4a | |||
| 51d6d981f1 | |||
| 820da31fe2 | |||
| 6281a44961 | |||
| 90a1c47389 | |||
| 782d0f1cb3 | |||
| f2cd26dda3 | |||
| 11e5b578a1 | |||
| 7f7e0fab6a | |||
| a0ff6ec812 | |||
| 840bc849d3 | |||
| 752bd2b026 | |||
| 8086ab59b9 | |||
| 948fcfda9a | |||
| daceb5d56d | |||
| e6eae4117f | |||
| a8c74095fe | |||
| f608f45f02 | |||
| 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 |
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
|
||||
35
Gemfile
@@ -1,22 +1,37 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
# "standard lib" gems
|
||||
gem "base64"
|
||||
gem "rexml"
|
||||
gem "logger"
|
||||
|
||||
# networking libs
|
||||
gem "async-http"
|
||||
gem "async-websocket"
|
||||
|
||||
# "game" library gem
|
||||
gem "cyberarm_engine"
|
||||
gem "sdl2-bindings"
|
||||
gem "digest-crc"
|
||||
gem "i18n"
|
||||
gem "ircparser"
|
||||
gem "rexml"
|
||||
gem "rubyzip"
|
||||
gem "websocket-client-simple"
|
||||
gem "win32-process", platforms: [:x64_mingw, :mingw]
|
||||
gem "win32-security", platforms: [:x64_mingw, :mingw]
|
||||
|
||||
# Packaging on 3.3.0 is... painful. Using 3.2.0 for now.
|
||||
# misc. libs
|
||||
gem "digest-crc"
|
||||
gem "ircparser"
|
||||
gem "rubyzip"
|
||||
|
||||
# file selection dialogs on windows (SDL3 has these built-in, but we're on SDL2)
|
||||
gem "libui", platforms: [:windows]
|
||||
# misc. windows only gems
|
||||
gem "win32-process", platforms: [:windows]
|
||||
gem "win32-security", platforms: [:windows]
|
||||
|
||||
# PACKAGING NOTES
|
||||
# bundler 2.5.x doesn't seem to play nice with ocra[n]
|
||||
# use `bundle _x.y.z_ COMMAND` to use this one...
|
||||
# NOTE: Releasy needs to be installed as a system gem i.e. `rake install`
|
||||
# NOTE: 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 "releasy", github: "cyberarm/releasy"
|
||||
# gem "ocran"
|
||||
# gem "releasy"#, path: "../releasy"
|
||||
# end
|
||||
|
||||
96
Gemfile.lock
@@ -1,33 +1,78 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
base64 (0.2.0)
|
||||
concurrent-ruby (1.2.3)
|
||||
cyberarm_engine (0.24.2)
|
||||
excon (~> 0.88)
|
||||
async (2.38.1)
|
||||
console (~> 1.29)
|
||||
fiber-annotation
|
||||
io-event (~> 1.11)
|
||||
metrics (~> 0.12)
|
||||
traces (~> 0.18)
|
||||
async-http (0.94.2)
|
||||
async (>= 2.10.2)
|
||||
async-pool (~> 0.11)
|
||||
io-endpoint (~> 0.14)
|
||||
io-stream (~> 0.6)
|
||||
metrics (~> 0.12)
|
||||
protocol-http (~> 0.58)
|
||||
protocol-http1 (~> 0.36)
|
||||
protocol-http2 (~> 0.22)
|
||||
protocol-url (~> 0.2)
|
||||
traces (~> 0.10)
|
||||
async-pool (0.11.2)
|
||||
async (>= 2.0)
|
||||
async-websocket (0.30.0)
|
||||
async-http (~> 0.76)
|
||||
protocol-http (~> 0.34)
|
||||
protocol-rack (~> 0.7)
|
||||
protocol-websocket (~> 0.17)
|
||||
base64 (0.3.0)
|
||||
console (1.34.3)
|
||||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
json
|
||||
cyberarm_engine (0.25.1)
|
||||
gosu (~> 1.1)
|
||||
gosu_more_drawables (~> 0.3)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
event_emitter (0.2.6)
|
||||
excon (0.109.0)
|
||||
ffi (1.16.3-x64-mingw-ucrt)
|
||||
ffi-win32-extensions (1.0.4)
|
||||
ffi
|
||||
ffi (1.17.0)
|
||||
ffi-win32-extensions (1.1.0)
|
||||
ffi (>= 1.15.5, <= 1.17.0)
|
||||
fiber-annotation (0.2.0)
|
||||
fiber-local (1.1.0)
|
||||
fiber-storage
|
||||
fiber-storage (1.0.1)
|
||||
fiddle (1.1.8)
|
||||
gosu (1.4.6)
|
||||
gosu_more_drawables (0.3.1)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
io-endpoint (0.17.2)
|
||||
io-event (1.14.4)
|
||||
io-stream (0.11.1)
|
||||
ircparser (1.0.0)
|
||||
rake (13.1.0)
|
||||
rexml (3.2.6)
|
||||
rubyzip (2.3.2)
|
||||
json (2.19.2)
|
||||
libui (0.2.0-x64-mingw-ucrt)
|
||||
fiddle
|
||||
logger (1.7.0)
|
||||
metrics (0.15.0)
|
||||
protocol-hpack (1.5.1)
|
||||
protocol-http (0.60.0)
|
||||
protocol-http1 (0.37.0)
|
||||
protocol-http (~> 0.58)
|
||||
protocol-http2 (0.24.0)
|
||||
protocol-hpack (~> 1.4)
|
||||
protocol-http (~> 0.47)
|
||||
protocol-rack (0.22.0)
|
||||
io-stream (>= 0.10)
|
||||
protocol-http (~> 0.58)
|
||||
rack (>= 1.0)
|
||||
protocol-url (0.4.0)
|
||||
protocol-websocket (0.20.2)
|
||||
protocol-http (~> 0.2)
|
||||
rack (3.2.5)
|
||||
rake (13.3.1)
|
||||
rexml (3.4.4)
|
||||
rubyzip (3.2.2)
|
||||
sdl2-bindings (0.2.3)
|
||||
ffi (~> 1.15)
|
||||
websocket (1.2.10)
|
||||
websocket-client-simple (0.8.0)
|
||||
event_emitter
|
||||
websocket
|
||||
traces (0.18.2)
|
||||
win32-process (0.10.0)
|
||||
ffi (>= 1.0.0)
|
||||
win32-security (0.5.0)
|
||||
@@ -36,19 +81,22 @@ GEM
|
||||
|
||||
PLATFORMS
|
||||
x64-mingw-ucrt
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
async-http
|
||||
async-websocket
|
||||
base64
|
||||
cyberarm_engine
|
||||
digest-crc
|
||||
i18n
|
||||
ircparser
|
||||
libui
|
||||
logger
|
||||
rexml
|
||||
rubyzip
|
||||
sdl2-bindings
|
||||
websocket-client-simple
|
||||
win32-process
|
||||
win32-security
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.3
|
||||
4.0.3
|
||||
|
||||
17
README.md
@@ -1,14 +1,21 @@
|
||||

|
||||
|
||||
# Cyberarm's Linux Friendly W3D Hub Launcher
|
||||
It runs natively on Linux! No mucking about trying to get .NET 4.6.1 or something installed in wine.
|
||||
Only requires OpenGL, Ruby, and a few gems.
|
||||
|
||||
## Installing
|
||||
* Install Ruby 3.0+, from your package manager.
|
||||
## Download
|
||||
[Download pre-built binaries.](https://github.com/cyberarm/w3d_hub_linux_launcher/releases)
|
||||
|
||||
## Development
|
||||
|
||||
### Installing
|
||||
* Install Ruby 3.4+, from your package manager.
|
||||
* Install Gosu's [dependencies](https://github.com/gosu/gosu/wiki/Getting-Started-on-Linux).
|
||||
* Install required gems: `bundle install`
|
||||
|
||||
## Usage
|
||||
### Usage
|
||||
`ruby w3d_hub_linux_launcher.rb`
|
||||
|
||||
## Contributing
|
||||
Contributors welcome, especially if anyone can lend a hand at reducing patching memory usage.
|
||||
### Contributing
|
||||
Contributors welcome.
|
||||
|
||||
284
lib/api.rb
@@ -1,38 +1,41 @@
|
||||
class W3DHub
|
||||
class Api
|
||||
|
||||
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
|
||||
DEFAULT_HEADERS = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application/json"
|
||||
}.freeze
|
||||
FORM_ENCODED_HEADERS = {
|
||||
"User-Agent": USER_AGENT,
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
}.freeze
|
||||
DEFAULT_HEADERS = [
|
||||
["user-agent", USER_AGENT],
|
||||
["accept", "application/json"]
|
||||
].freeze
|
||||
FORM_ENCODED_HEADERS = [
|
||||
["user-agent", USER_AGENT],
|
||||
["accept", "application/json"],
|
||||
["content-type", "application/x-www-form-urlencoded"]
|
||||
].freeze
|
||||
|
||||
def self.on_thread(method, *args, &callback)
|
||||
BackgroundWorker.foreground_job(-> { Api.send(method, *args) }, callback)
|
||||
end
|
||||
|
||||
class DummyResponse
|
||||
def initialize(error)
|
||||
class Response
|
||||
def initialize(error: nil, status: -1, body: "")
|
||||
@status = status
|
||||
@body = body
|
||||
@error = error
|
||||
end
|
||||
|
||||
def success?
|
||||
false
|
||||
@status == 200
|
||||
end
|
||||
|
||||
def status
|
||||
-1
|
||||
@status
|
||||
end
|
||||
|
||||
def body
|
||||
""
|
||||
@body
|
||||
end
|
||||
|
||||
def error
|
||||
@@ -41,49 +44,86 @@ class W3DHub
|
||||
end
|
||||
|
||||
#! === W3D Hub API === !#
|
||||
W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze #
|
||||
ALT_W3DHUB_API_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze #
|
||||
|
||||
ENDPOINT = "https://secure.w3dhub.com".freeze
|
||||
HTTP_CLIENTS = {}
|
||||
|
||||
def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
|
||||
case backend
|
||||
when :w3dhub
|
||||
endpoint = W3DHUB_API_ENDPOINT
|
||||
when :alt_w3dhub
|
||||
endpoint = ALT_W3DHUB_API_ENDPOINT
|
||||
when :gsh
|
||||
endpoint = SERVER_LIST_ENDPOINT
|
||||
end
|
||||
|
||||
# Handle arbitrary urls that may come through
|
||||
url = nil
|
||||
if path.start_with?("http")
|
||||
uri = URI(path)
|
||||
endpoint = uri.origin
|
||||
path = uri.request_uri
|
||||
else
|
||||
url = "#{endpoint}#{path}"
|
||||
end
|
||||
|
||||
def self.excon(method, url, headers = DEFAULT_HEADERS, body = nil)
|
||||
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
|
||||
|
||||
# Inject Authorization header if account data is populated
|
||||
if Store.account
|
||||
logger.debug(LOG_TAG) { " Injecting Authorization header..." }
|
||||
headers = headers.dup
|
||||
headers["Authorization"] = "Bearer #{Store.account.access_token}"
|
||||
headers << ["authorization", "Bearer #{Store.account.access_token}"]
|
||||
end
|
||||
|
||||
begin
|
||||
Excon.send(
|
||||
method,
|
||||
url,
|
||||
headers: headers,
|
||||
body: body,
|
||||
nonblock: true,
|
||||
tcp_nodelay: true,
|
||||
write_timeout: API_TIMEOUT,
|
||||
read_timeout: API_TIMEOUT,
|
||||
connect_timeout: API_TIMEOUT,
|
||||
idempotent: true,
|
||||
retry_limit: 3,
|
||||
retry_interval: 1,
|
||||
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
|
||||
)
|
||||
rescue Excon::Errors::Timeout => e
|
||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
||||
Sync do
|
||||
begin
|
||||
response = provision_http_client(endpoint).send(method, path, headers, body)
|
||||
|
||||
DummyResponse.new(e)
|
||||
rescue Excon::Error => e
|
||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
|
||||
logger.error(LOG_TAG) { e }
|
||||
Response.new(status: response.status, body: response.read)
|
||||
rescue Async::TimeoutError => e
|
||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
||||
|
||||
DummyResponse.new(e)
|
||||
Response.new(error: e)
|
||||
rescue StandardError => e
|
||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
|
||||
logger.error(LOG_TAG) { e }
|
||||
|
||||
Response.new(error: e)
|
||||
ensure
|
||||
response&.close
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.post(url, headers = DEFAULT_HEADERS, body = nil)
|
||||
excon(:post, url, headers, body)
|
||||
def self.provision_http_client(hostname)
|
||||
# Pin http clients to their host Thread so the fiber scheduler doesn't get upset and raise an error
|
||||
HTTP_CLIENTS[Thread.current] ||= {}
|
||||
return HTTP_CLIENTS[Thread.current][hostname.downcase] if HTTP_CLIENTS[Thread.current][hostname.downcase]
|
||||
|
||||
ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil
|
||||
ssl_context&.set_params(
|
||||
ca_file: W3DHub.ca_bundle_path,
|
||||
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
||||
)
|
||||
|
||||
endpoint = Async::HTTP::Endpoint.parse(hostname, ssl_context: ssl_context)
|
||||
HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint)
|
||||
end
|
||||
|
||||
def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
|
||||
async_http(:post, path, headers, body, backend)
|
||||
end
|
||||
|
||||
def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
|
||||
async_http(:get, path, headers, body, backend)
|
||||
end
|
||||
|
||||
# Api.get but handles any URL instead of known hosts
|
||||
def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil)
|
||||
async_http(:get, path, headers, body, backend)
|
||||
end
|
||||
|
||||
# Method: POST
|
||||
@@ -101,24 +141,16 @@ class W3DHub
|
||||
#
|
||||
# On a failed login the service responds with:
|
||||
# {"error":"login-failed"}
|
||||
def self.refresh_user_login(refresh_token)
|
||||
body = "data=#{JSON.dump({refreshToken: refresh_token})}"
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body)
|
||||
def self.refresh_user_login(refresh_token, backend = :w3dhub)
|
||||
body = URI.encode_www_form("data": JSON.dump({refreshToken: refresh_token}))
|
||||
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend)
|
||||
|
||||
if response.status == 200
|
||||
user_data = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
return false if user_data[:error]
|
||||
|
||||
body = "data=#{JSON.dump({ id: user_data[:userid] })}"
|
||||
user_details = post("#{ENDPOINT}/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if user_details.status == 200
|
||||
user_details_data = JSON.parse(user_details.body, symbolize_names: true)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch refresh user details:" }
|
||||
logger.error(LOG_TAG) { user_details }
|
||||
end
|
||||
user_details_data = user_details(user_data[:userid]) || {}
|
||||
|
||||
Account.new(user_data, user_details_data)
|
||||
else
|
||||
@@ -129,24 +161,16 @@ class W3DHub
|
||||
end
|
||||
|
||||
# See #user_refresh_token
|
||||
def self.user_login(username, password)
|
||||
body = "data=#{JSON.dump({username: username, password: password})}"
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body)
|
||||
def self.user_login(username, password, backend = :w3dhub)
|
||||
body = URI.encode_www_form("data": JSON.dump({username: username, password: password}))
|
||||
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend)
|
||||
|
||||
if response.status == 200
|
||||
user_data = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
return false if user_data[:error]
|
||||
|
||||
body = "data=#{JSON.dump({ id: user_data[:userid] })}"
|
||||
user_details = post("#{ENDPOINT}/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body)
|
||||
|
||||
if user_details.status == 200
|
||||
user_details_data = JSON.parse(user_details.body, symbolize_names: true)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch user details:" }
|
||||
logger.error(LOG_TAG) { user_details }
|
||||
end
|
||||
user_details_data = user_details(user_data[:userid]) || {}
|
||||
|
||||
Account.new(user_data, user_details_data)
|
||||
else
|
||||
@@ -156,18 +180,27 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
# /apis/launcher/1/user-login
|
||||
# Client sends an Authorization header bearer token which is received from logging in (Required?)
|
||||
# /apis/w3dhub/1/get-user-details
|
||||
#
|
||||
# Response: avatar-uri (Image download uri), id, username
|
||||
def self.user_details(id)
|
||||
def self.user_details(id, backend = :w3dhub)
|
||||
body = URI.encode_www_form("data": JSON.dump({ id: id }))
|
||||
user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend)
|
||||
|
||||
if user_details.status == 200
|
||||
JSON.parse(user_details.body, symbolize_names: true)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch user details:" }
|
||||
logger.error(LOG_TAG) { user_details }
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# /apis/w3dhub/1/get-service-status
|
||||
# Service response:
|
||||
# {"services":{"authentication":true,"packageDownload":true}}
|
||||
def self.service_status
|
||||
response = post("#{ENDPOINT}/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS)
|
||||
def self.service_status(backend = :w3dhub)
|
||||
response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend)
|
||||
|
||||
if response.status == 200
|
||||
ServiceStatus.new(response.body)
|
||||
@@ -182,11 +215,11 @@ class W3DHub
|
||||
# Client sends an Authorization header bearer token which is received from logging in (Optional)
|
||||
# Launcher sends an empty data request: data={}
|
||||
# Response is a list of applications/games
|
||||
def self.applications
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/get-applications")
|
||||
def self.applications(backend = :w3dhub)
|
||||
response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend)
|
||||
|
||||
if response.status == 200
|
||||
Applications.new(response.body)
|
||||
Applications.new(response.body, backend)
|
||||
else
|
||||
logger.error(LOG_TAG) { "Failed to fetch applications list:" }
|
||||
logger.error(LOG_TAG) { response }
|
||||
@@ -194,13 +227,82 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
# Populate applications list from primary and alternate backends
|
||||
# (alternate only has latest public builds of _most_ games)
|
||||
def self._applications
|
||||
applications_primary = Store.account ? Api.applications(:w3dhub) : false
|
||||
applications_alternate = Api.applications(:alt_w3dhub)
|
||||
|
||||
# Fail if we fail to fetch applications list from either backend
|
||||
return false unless applications_primary || applications_alternate
|
||||
|
||||
return applications_alternate unless applications_primary
|
||||
|
||||
# Merge the two app lists together
|
||||
apps = applications_alternate
|
||||
if applications_primary
|
||||
applications_primary.games.each do |game|
|
||||
# Check if game exists in alternate list
|
||||
_game = apps.games.find { |g| g.id == game.id }
|
||||
unless _game
|
||||
apps.games << game
|
||||
|
||||
# App didn't exist in alternates list
|
||||
# comparing channels isn't useful
|
||||
next
|
||||
end
|
||||
|
||||
# If it does, check that all of its channels also exist in alternate list
|
||||
# and that the primary versions are the same as the alternates list
|
||||
game.channels.each do |channel|
|
||||
_channel = _game.channels.find { |c| c.id == channel.id }
|
||||
|
||||
unless _channel
|
||||
_game.channels << channel
|
||||
|
||||
# App didn't have channel in alternates list
|
||||
# comparing channel isn't useful
|
||||
next
|
||||
end
|
||||
|
||||
# If channel versions and access levels match then all's well
|
||||
if channel.current_version == _channel.current_version &&
|
||||
channel.user_level == _channel.user_level
|
||||
|
||||
# All's Well!
|
||||
next
|
||||
end
|
||||
|
||||
# If the access levels 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
|
||||
# Client sends an Authorization header bearer token which is received from logging in (Optional)
|
||||
# Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates)
|
||||
# Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one
|
||||
def self.news(category)
|
||||
body = "data=#{JSON.dump({category: category})}"
|
||||
response = post("#{ENDPOINT}/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body)
|
||||
def self.news(category, backend = :w3dhub)
|
||||
body = URI.encode_www_form("data": JSON.dump({category: category}))
|
||||
response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend)
|
||||
|
||||
if response.status == 200
|
||||
News.new(response.body)
|
||||
@@ -216,9 +318,9 @@ class W3DHub
|
||||
|
||||
# /apis/launcher/1/get-package-details
|
||||
# client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]}
|
||||
def self.package_details(packages)
|
||||
def self.package_details(packages, backend = :w3dhub)
|
||||
body = URI.encode_www_form("data": JSON.dump({ packages: packages }))
|
||||
response = post("#{ENDPOINT}/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body)
|
||||
response = post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend)
|
||||
|
||||
if response.status == 200
|
||||
hash = JSON.parse(response.body, symbolize_names: true)
|
||||
@@ -243,9 +345,9 @@ class W3DHub
|
||||
# /apis/w3dhub/1/get-events
|
||||
#
|
||||
# clients requests events: data={"serverPath":"apb"}
|
||||
def self.events(app_id)
|
||||
def self.events(app_id, backend = :w3dhub)
|
||||
body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id }))
|
||||
response = post("#{ENDPOINT}/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body)
|
||||
response = post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend)
|
||||
|
||||
if response.status == 200
|
||||
array = JSON.parse(response.body, symbolize_names: true)
|
||||
@@ -257,11 +359,9 @@ class W3DHub
|
||||
|
||||
#! === Server List API === !#
|
||||
|
||||
SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze
|
||||
|
||||
def self.get(url, headers = DEFAULT_HEADERS, body = nil)
|
||||
excon(:get, url, headers, body)
|
||||
end
|
||||
# SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze
|
||||
SERVER_LIST_ENDPOINT = "https://gsh.w3d.cyberarm.dev".freeze
|
||||
# SERVER_LIST_ENDPOINT = "http://127.0.0.1:9292".freeze
|
||||
|
||||
# Method: GET
|
||||
# FORMAT: JSON
|
||||
@@ -280,8 +380,8 @@ class W3DHub
|
||||
# id, name, score, kills, deaths
|
||||
# ...players[]:
|
||||
# nick, team (index of teams array), score, kills, deaths
|
||||
def self.server_list(level = 1)
|
||||
response = get("#{SERVER_LIST_ENDPOINT}/listings/getAll/v2?statusLevel=#{level}")
|
||||
def self.server_list(level = 1, backend = :gsh)
|
||||
response = get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend)
|
||||
|
||||
if response.status == 200
|
||||
data = JSON.parse(response.body, symbolize_names: true)
|
||||
@@ -302,8 +402,10 @@ class W3DHub
|
||||
# id, name, score, kills, deaths
|
||||
# ...players[]:
|
||||
# nick, team (index of teams array), score, kills, deaths
|
||||
def self.server_details(id, level)
|
||||
response = get("#{SERVER_LIST_ENDPOINT}/listings/getStatus/v2/#{id}?statusLevel=#{level}")
|
||||
def self.server_details(id, level, backend = :gsh)
|
||||
return false unless id && level
|
||||
|
||||
response = get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend)
|
||||
|
||||
if response.status == 200
|
||||
hash = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
@@ -24,8 +24,9 @@ class W3DHub
|
||||
|
||||
def to_json(env)
|
||||
d = @data.dup
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
class W3DHub
|
||||
class Api
|
||||
class Applications
|
||||
def initialize(response)
|
||||
attr_reader :data
|
||||
|
||||
def initialize(response, source = nil)
|
||||
@data = JSON.parse(response, symbolize_names: true)
|
||||
|
||||
games = @data[:applications].select { |a| a[:category] == "games" }
|
||||
|
||||
@games = []
|
||||
|
||||
games.each { |hash| @games << Game.new(hash) }
|
||||
games.each { |hash| @games << Game.new(hash, source) }
|
||||
@games.sort_by!(&:name).reverse
|
||||
end
|
||||
|
||||
@@ -18,9 +20,11 @@ class W3DHub
|
||||
|
||||
class Game
|
||||
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[:___source] = source if source
|
||||
|
||||
@id = @data[:id].to_s
|
||||
@name = @data[:name]
|
||||
@@ -29,26 +33,58 @@ class W3DHub
|
||||
@studio_id = @data[:"studio-id"]
|
||||
|
||||
# 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) } || []
|
||||
@extended_data = @data[:"extended-data"]
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
class Channel
|
||||
attr_reader :id, :name, :user_level, :current_version
|
||||
|
||||
def initialize(hash)
|
||||
def initialize(hash, source = nil)
|
||||
@data = hash
|
||||
@data[:___source] = source
|
||||
|
||||
@id = @data[:id].to_s
|
||||
@name = @data[:name]
|
||||
@user_level = @data[:"user-level"]
|
||||
@current_version = @data[:"current-version"]
|
||||
end
|
||||
|
||||
def source
|
||||
@data[:___source]&.to_sym || :w3dhub
|
||||
end
|
||||
|
||||
def source=(sym)
|
||||
@data[:___source] = sym
|
||||
end
|
||||
end
|
||||
|
||||
class WebLink
|
||||
|
||||
@@ -6,7 +6,7 @@ class W3DHub
|
||||
def initialize(response)
|
||||
@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
|
||||
|
||||
class Item
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class W3DHub
|
||||
class Api
|
||||
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
|
||||
|
||||
def initialize(hash)
|
||||
@@ -16,6 +16,9 @@ class W3DHub
|
||||
@checksum = @data[:checksum]
|
||||
@checksum_chunk_size = @data[:"checksum-chunk-size"]
|
||||
@checksum_chunks = @data[:"checksum-chunks"]
|
||||
@error = @data[:error] || nil
|
||||
|
||||
@download_url = @data[:download_url] || nil
|
||||
|
||||
@custom_partially_valid_at_bytes = 0
|
||||
@custom_is_patch = false
|
||||
@@ -25,6 +28,10 @@ class W3DHub
|
||||
@checksum_chunks[:"#{key}"]
|
||||
end
|
||||
|
||||
def error?
|
||||
@error
|
||||
end
|
||||
|
||||
def partially_valid_at_bytes=(i)
|
||||
@custom_partially_valid_at_bytes = i
|
||||
end
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
class W3DHub
|
||||
class Api
|
||||
class ServerListServer
|
||||
attr_reader :id, :game, :address, :port, :region, :channel, :ping, :status
|
||||
NO_OR_BAD_PING = 1_000_000
|
||||
NO_OR_DEFAULT_VERSION = "838"
|
||||
|
||||
attr_reader :id, :game, :address, :port, :region, :channel, :version, :ping, :status
|
||||
|
||||
def initialize(hash)
|
||||
@data = hash
|
||||
@@ -12,12 +15,13 @@ class W3DHub
|
||||
@port = @data[:port]
|
||||
@region = @data[:region]
|
||||
@channel = @data[:channel] || "release"
|
||||
@ping = -1
|
||||
@version = @data[:version] || NO_OR_DEFAULT_VERSION
|
||||
@ping = NO_OR_BAD_PING
|
||||
|
||||
@status = @data[:status] ? Status.new(@data[:status]) : nil
|
||||
@status = Status.new(@data[:status])
|
||||
|
||||
@ping_interval = 30_000
|
||||
@last_pinged = Gosu.milliseconds + @ping_interval + 1
|
||||
@last_pinged = Gosu.milliseconds + @ping_interval + 1_000
|
||||
end
|
||||
|
||||
def update(hash)
|
||||
@@ -34,11 +38,11 @@ class W3DHub
|
||||
@status.players = hash[:players]&.select { |t| t[:nick] != "Nod" && t[:nick] != "GDI" }&.map { |t| Player.new(t) } if hash[:players]
|
||||
|
||||
send_ping
|
||||
|
||||
return true
|
||||
else
|
||||
@status = Status.new(hash)
|
||||
end
|
||||
|
||||
false
|
||||
true
|
||||
end
|
||||
|
||||
def send_ping(force_ping = false)
|
||||
@@ -55,7 +59,7 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
@ping = -1 if @ping.zero?
|
||||
@ping = NO_OR_BAD_PING if @ping.zero?
|
||||
|
||||
@ping
|
||||
end,
|
||||
@@ -70,18 +74,18 @@ class W3DHub
|
||||
attr_accessor :name, :password, :map, :max_players, :player_count, :started, :remaining, :teams, :players
|
||||
|
||||
def initialize(hash)
|
||||
@data = hash
|
||||
@data = hash || {}
|
||||
|
||||
@teams = @data[:teams]&.map { |t| Team.new(t) }
|
||||
@players = @data[:players]&.select { |t| t[:nick] != "Nod" && t[:nick] != "GDI" }&.map { |t| Player.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) } || []
|
||||
|
||||
@name = @data[:name]
|
||||
@name = @data[:name] || ""
|
||||
@password = @data[:password] || false
|
||||
@map = @data[:map]
|
||||
@max_players = @data[:maxplayers]
|
||||
@map = @data[:map] || ""
|
||||
@max_players = @data[:maxplayers] || 0
|
||||
@player_count = @players.size || @data[:numplayers].to_i
|
||||
@started = @data[:started]
|
||||
@remaining = @data[:remaining]
|
||||
@started = @data[:started] || Time.now
|
||||
@remaining = @data[:remaining] || "00.00.00"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -2,6 +2,9 @@ class W3DHub
|
||||
class Api
|
||||
class ServerListUpdater
|
||||
LOG_TAG = "W3DHub::Api::ServerListUpdater".freeze
|
||||
|
||||
TYPE_PING = 6
|
||||
|
||||
include CyberarmEngine::Common
|
||||
@@instance = nil
|
||||
|
||||
@@ -11,10 +14,13 @@ class W3DHub
|
||||
@@instance = ServerListUpdater.new
|
||||
end
|
||||
|
||||
attr_accessor :auto_reconnect
|
||||
attr_accessor :auto_reconnect, :invocation_id
|
||||
|
||||
def initialize
|
||||
@auto_reconnect = false
|
||||
@reconnection_delay = 1
|
||||
|
||||
@invocation_id = 0
|
||||
|
||||
logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." }
|
||||
run
|
||||
@@ -22,19 +28,21 @@ class W3DHub
|
||||
|
||||
def run
|
||||
Thread.new do
|
||||
begin
|
||||
connect
|
||||
Sync do |task|
|
||||
begin
|
||||
@auto_reconnect = true
|
||||
|
||||
while W3DHub::BackgroundWorker.alive?
|
||||
connect if @auto_reconnect
|
||||
sleep 1
|
||||
while W3DHub::BackgroundWorker.alive?
|
||||
connect if @auto_reconnect
|
||||
sleep @reconnection_delay
|
||||
end
|
||||
rescue => e
|
||||
puts e
|
||||
puts e.backtrace
|
||||
|
||||
sleep 30
|
||||
retry
|
||||
end
|
||||
rescue => e
|
||||
puts e
|
||||
puts e.backtrace
|
||||
|
||||
sleep 10
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,62 +54,173 @@ class W3DHub
|
||||
@auto_reconnect = false
|
||||
|
||||
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
|
||||
@reconnection_delay = @reconnection_delay * 2
|
||||
@reconnection_delay = 60 if @reconnection_delay > 60
|
||||
return
|
||||
end
|
||||
|
||||
@reconnection_delay = 1
|
||||
|
||||
data = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
@invocation_id = 0 if @invocation_id > 9095
|
||||
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..." }
|
||||
this = self
|
||||
WebSocket::Client::Simple.connect(endpoint, headers: Api::DEFAULT_HEADERS) do |ws|
|
||||
@ws = WebSocketClient.new.connect(endpoint, headers: Api::DEFAULT_HEADERS) do |ws|
|
||||
ws.on(:open) do
|
||||
logger.debug(LOG_TAG) { "Requesting json protocol, v1..." }
|
||||
ws.send({ protocol: "json", version: 1 }.to_json + "\x1e")
|
||||
|
||||
logger.debug(LOG_TAG) { "Subscribing to server changes..." }
|
||||
Store.server_list.each_with_index do |server, i|
|
||||
i += 1
|
||||
Store.server_list.each do |server|
|
||||
this.invocation_id += 1
|
||||
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")
|
||||
end
|
||||
end
|
||||
|
||||
ws.on(:message) do |msg|
|
||||
msg = msg.data.split("\x1e").first
|
||||
msg = msg.to_str.split("\x1e").first
|
||||
|
||||
hash = JSON.parse(msg, symbolize_names: true)
|
||||
|
||||
# Send PING(?)
|
||||
if hash.empty? || hash[:type] == 6
|
||||
ws.send({ type: 6 }.to_json + "\x1e")
|
||||
else
|
||||
case hash[:type]
|
||||
when 1
|
||||
if hash[:target] == "ServerStatusChanged"
|
||||
id, data = hash[:arguments]
|
||||
server = Store.server_list.find { |s| s.id == id }
|
||||
server_updated = server&.update(data)
|
||||
# pp hash if hash[:target] != "ServerStatusChanged" && hash[:type] != 6 && hash[:type] != 3
|
||||
|
||||
BackgroundWorker.foreground_job(-> {}, ->(result){ States::Interface.instance&.update_server_browser(server) }) if server_updated
|
||||
# Send PING(?)
|
||||
if hash.empty? || hash[:type] == TYPE_PING
|
||||
ws.send({ type: TYPE_PING }.to_json + "\x1e")
|
||||
next
|
||||
end
|
||||
|
||||
case hash[:type]
|
||||
when 1
|
||||
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]
|
||||
server = Store.server_list.find { |s| s.id == id }
|
||||
server_updated = server&.update(data)
|
||||
|
||||
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
|
||||
|
||||
ws.on(:close) do |e|
|
||||
p e
|
||||
ws.on(:close) do
|
||||
logger.error(LOG_TAG) { "Connection closed." }
|
||||
this.auto_reconnect = true
|
||||
ws.close
|
||||
end
|
||||
|
||||
ws.on(:error) do |e|
|
||||
p e
|
||||
logger.error(LOG_TAG) { e }
|
||||
this.auto_reconnect = true
|
||||
ws.close
|
||||
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
|
||||
|
||||
@@ -5,6 +5,7 @@ class W3DHub
|
||||
|
||||
def initialize
|
||||
@tasks = [] # :installer, :importer, :repairer, :uninstaller
|
||||
@running_applications = {}
|
||||
end
|
||||
|
||||
def install(app_id, channel)
|
||||
@@ -22,9 +23,7 @@ class W3DHub
|
||||
# unpack packages
|
||||
# install dependencies (e.g. visual C runtime)
|
||||
|
||||
installer = Installer.new(app_id, channel)
|
||||
|
||||
@tasks.push(installer)
|
||||
@tasks.push(Installer.new(app_id, channel))
|
||||
end
|
||||
|
||||
def update(app_id, channel)
|
||||
@@ -32,9 +31,7 @@ class W3DHub
|
||||
|
||||
return false unless installed?(app_id, channel)
|
||||
|
||||
updater = Updater.new(app_id, channel)
|
||||
|
||||
@tasks.push(updater)
|
||||
@tasks.push(Updater.new(app_id, channel))
|
||||
end
|
||||
|
||||
def import(app_id, channel)
|
||||
@@ -44,7 +41,7 @@ class W3DHub
|
||||
# if auto-import fails ask user for path to game exe
|
||||
# mark app as imported/installed
|
||||
|
||||
@tasks.push(Importer.new(app_id, channel))
|
||||
push_state(W3DHub::States::ImportGameDialog, app_id: app_id, channel: channel)
|
||||
end
|
||||
|
||||
def settings(app_id, channel)
|
||||
@@ -86,15 +83,15 @@ class W3DHub
|
||||
|
||||
# open wwconfig.exe or config.exe for ecw
|
||||
|
||||
if (app_data = installed?(app_id, channel) && W3DHub.unix?)
|
||||
exe = if Store.settings[:wine_prefix]
|
||||
"WINEPREFIX=\"#{Store.settings[:wine_prefix]}\" winecfg"
|
||||
else
|
||||
"winecfg"
|
||||
end
|
||||
return unless (app_data = installed?(app_id, channel) && W3DHub.unix?)
|
||||
|
||||
Process.spawn("#{exe}")
|
||||
end
|
||||
exe = if !Store.settings[:wine_prefix].to_s.empty?
|
||||
"WINEPREFIX=\"#{Store.settings[:wine_prefix]}\" winecfg"
|
||||
else
|
||||
"winecfg"
|
||||
end
|
||||
|
||||
Process.spawn(exe)
|
||||
end
|
||||
|
||||
def repair(app_id, channel)
|
||||
@@ -169,11 +166,16 @@ class W3DHub
|
||||
def wine_command(app_id, channel)
|
||||
return "" if W3DHub.windows?
|
||||
|
||||
if Store.settings[:wine_prefix]
|
||||
"WINEPREFIX=\"#{Store.settings[:wine_prefix]}\" \"#{Store.settings[:wine_command]}\" "
|
||||
else
|
||||
"#{Store.settings[:wine_command]} "
|
||||
end
|
||||
"\"#{Store.settings[:wine_command]}\" "
|
||||
end
|
||||
|
||||
def wine_enviroment_variables(app_id, channel)
|
||||
vars = {}
|
||||
return vars if W3DHub.windows?
|
||||
|
||||
vars["WINEPREFIX"] = Store.settings[:wine_prefix] unless Store.settings[:wine_prefix].to_s.empty?
|
||||
|
||||
vars
|
||||
end
|
||||
|
||||
def mangohud_command(app_id, channel)
|
||||
@@ -188,6 +190,13 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
def mangohud_enviroment_variables(app_id, channel)
|
||||
vars = {}
|
||||
return vars if W3DHub.windows?
|
||||
|
||||
vars
|
||||
end
|
||||
|
||||
def dxvk_command(app_id, channel)
|
||||
return "" if W3DHub.windows?
|
||||
|
||||
@@ -201,37 +210,97 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
def run(app_id, channel, *args)
|
||||
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?
|
||||
def dxvk_enviroment_variables(app_id, channel)
|
||||
vars = {}
|
||||
return vars if W3DHub.windows?
|
||||
|
||||
pid = Process.spawn("#{dxvk_command(app_id, channel)}#{mangohud_command(app_id, channel)}#{wine_command(app_id, channel)}\"#{exe_path}\" -launcher #{args.join(' ')}")
|
||||
Process.detach(pid)
|
||||
vars
|
||||
end
|
||||
|
||||
def start_command(path, exe)
|
||||
if W3DHub.windows?
|
||||
"start /D \"#{path}\" /B #{exe}"
|
||||
else
|
||||
"#{path}/#{exe}"
|
||||
end
|
||||
end
|
||||
|
||||
def join_server(app_id, channel, server, password = nil)
|
||||
if installed?(app_id, channel) && Store.settings[:server_list_username].to_s.length.positive?
|
||||
run(
|
||||
app_id, channel,
|
||||
"+connect #{server.address}:#{server.port} +netplayername #{Store.settings[:server_list_username]}#{password ? " +password \"#{password}\"" : ""}"
|
||||
)
|
||||
def run(app_id, channel, *args)
|
||||
if (app_data = installed?(app_id, channel))
|
||||
install_directory = app_data[:install_directory]
|
||||
exe_path = app_id == "ecw" ? "#{install_directory}/game500.exe" : app_data[:install_path]
|
||||
exe_path.gsub!("/", "\\") if W3DHub.windows?
|
||||
exe_path.gsub!("\\", "/") if W3DHub.unix?
|
||||
|
||||
exe = File.basename(exe_path)
|
||||
path = File.dirname(exe_path)
|
||||
|
||||
env = {}
|
||||
if W3DHub.unix?
|
||||
env.merge!(
|
||||
dxvk_enviroment_variables(app_id, channel),
|
||||
mangohud_enviroment_variables(app_id, channel),
|
||||
wine_enviroment_variables(app_id, channel)
|
||||
)
|
||||
end
|
||||
|
||||
attempted = false
|
||||
begin
|
||||
pid = Process.spawn(
|
||||
env,
|
||||
"#{dxvk_command(app_id, channel)}"\
|
||||
"#{mangohud_command(app_id, channel)}"\
|
||||
"#{wine_command(app_id, channel)}"\
|
||||
"#{attempted ? start_command(path, exe) : "\"#{exe_path}\""} "\
|
||||
"-launcher #{args.join(' ')}"
|
||||
)
|
||||
Process.detach(pid)
|
||||
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)
|
||||
return unless installed?(app_id, channel) && username.to_s.length.positive?
|
||||
|
||||
run(
|
||||
app_id, channel,
|
||||
"+connect #{server.address}:#{server.port} "\
|
||||
"+netplayername #{username}#{password ? " +password \"#{password}\"" : ""}"\
|
||||
"#{multi ? " +multi" : ""}"
|
||||
)
|
||||
end
|
||||
|
||||
def play_now_server(app_id, channel)
|
||||
app_data = installed?(app_id, channel)
|
||||
|
||||
return nil unless app_data
|
||||
|
||||
found_server = Store.server_list.select do |server|
|
||||
server.game == app_id && server.channel == channel && !server.status.password
|
||||
end&.first
|
||||
server_options = Store.server_list.select do |server|
|
||||
server.game == app_id &&
|
||||
server.channel == channel &&
|
||||
!server.status.password &&
|
||||
server.status.player_count < server.status.max_players
|
||||
end
|
||||
# sort by player count HIGH to LOW
|
||||
# and by ping LOW to HIGH
|
||||
server_options.sort! do |a, b|
|
||||
[b.status.player_count, a.ping] <=> [a.status.player_count, b.ping]
|
||||
end
|
||||
|
||||
found_server ? found_server : nil
|
||||
# try to find server with lowest ping and matching version
|
||||
found_server = server_options.find { |server| server.version == app_data[:installed_version] }
|
||||
# try to find server with lowest ping and undefined version
|
||||
found_server ||= server_options.find { |server| server.version == Api::ServerListServer::NO_OR_DEFAULT_VERSION }
|
||||
|
||||
found_server
|
||||
end
|
||||
|
||||
def play_now(app_id, channel)
|
||||
@@ -261,7 +330,7 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
def favorive(app_id, bool)
|
||||
def favorite(app_id, bool)
|
||||
Store.settings[:favorites] ||= {}
|
||||
|
||||
if bool
|
||||
@@ -319,7 +388,8 @@ class W3DHub
|
||||
|
||||
begin
|
||||
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!("\\", "/")
|
||||
|
||||
exe_path = app_id == "ecw" ? "#{install_path}/game500.exe" : "#{install_path}/game.exe"
|
||||
@@ -388,17 +458,19 @@ class W3DHub
|
||||
false
|
||||
end
|
||||
|
||||
def imported!(task, exe_path)
|
||||
def imported!(application, channel, exe_path)
|
||||
exe_path.gsub!("\\", "/")
|
||||
|
||||
application_data = {
|
||||
name: task.application.name,
|
||||
name: application.name,
|
||||
install_directory: File.dirname(exe_path),
|
||||
installed_version: task.channel.current_version,
|
||||
installed_version: channel.current_version,
|
||||
install_path: exe_path,
|
||||
wine_prefix: task.wine_prefix
|
||||
wine_prefix: nil
|
||||
}
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -473,6 +545,17 @@ class W3DHub
|
||||
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
|
||||
def idle?
|
||||
!busy?
|
||||
|
||||
@@ -27,8 +27,9 @@ class W3DHub
|
||||
@bytes_downloaded = -1
|
||||
|
||||
@manifests = []
|
||||
@files = {}
|
||||
@files = []
|
||||
@packages = []
|
||||
@deleted_files = [] # TODO: remove removed files
|
||||
|
||||
@wine_prefix = nil
|
||||
|
||||
@@ -77,7 +78,7 @@ class W3DHub
|
||||
@task_state = :complete unless @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
|
||||
@@ -111,6 +112,38 @@ class W3DHub
|
||||
@task_state == :failed
|
||||
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
|
||||
@task_failure_reason || ""
|
||||
end
|
||||
@@ -150,7 +183,8 @@ class W3DHub
|
||||
# Wine present?
|
||||
if W3DHub.unix?
|
||||
wine_present = W3DHub.command("which #{Store.settings[:wine_command]}")
|
||||
fail!("FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed. Will be unable to create prefixes or launch games.") unless wine_present
|
||||
fail!("FAIL FAST: `which #{Store.settings[:wine_command]}` command failed, wine is not installed.\n\n"\
|
||||
"Will be unable to launch game.\n\nCheck wine options in launcher's settings.") unless wine_present
|
||||
end
|
||||
end
|
||||
|
||||
@@ -207,16 +241,23 @@ class W3DHub
|
||||
@manifests << manifest
|
||||
|
||||
until(manifest.full?)
|
||||
fetch_manifest("games", app_id, "manifest.xml", manifest.base_version)
|
||||
manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version)
|
||||
manifests << manifest
|
||||
if fetch_manifest("games", app_id, "manifest.xml", manifest.base_version)
|
||||
manifest = load_manifest("games", app_id, "manifest.xml", manifest.base_version)
|
||||
manifests << manifest
|
||||
else
|
||||
fail!("Failed to retrieve manifest: games:#{app_id}:manifest.xml-#{manifest.base_version}")
|
||||
return []
|
||||
end
|
||||
end
|
||||
else
|
||||
fail!("Failed to retrieve manifest: games:#{app_id}:manifest.xml-#{@target_version}")
|
||||
return []
|
||||
end
|
||||
|
||||
@manifests
|
||||
end
|
||||
|
||||
def build_package_list(manifests)
|
||||
def build_package_list
|
||||
@status.operations.clear
|
||||
@status.label = "Downloading #{@application.name}..."
|
||||
@status.value = "Building package list..."
|
||||
@@ -224,35 +265,34 @@ class W3DHub
|
||||
|
||||
@status.step = :build_package_list
|
||||
|
||||
files = []
|
||||
packages = []
|
||||
deleted_files = [] # TODO: remove removed files
|
||||
|
||||
manifests.reverse.each do |manifest|
|
||||
# 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|
|
||||
logger.info(LOG_TAG) { "#{manifest.game}-#{manifest.type}: #{manifest.version} (#{manifest.base_version})" }
|
||||
|
||||
manifest.files.each do |file|
|
||||
@files["#{file.name}:#{manifest.version}"] = file
|
||||
|
||||
if file.removed? # No package data
|
||||
files.delete_if { |f| f.name == file.name }
|
||||
deleted_files.push(file)
|
||||
@files.delete_if { |f| f.name.casecmp?(file.name) }
|
||||
@deleted_files.push(file)
|
||||
next
|
||||
end
|
||||
|
||||
files.delete_if { |f| f.name == file.name } unless file.patch?
|
||||
@files.delete_if { |f| f.name.casecmp?(file.name) } unless file.patch?
|
||||
|
||||
files.push(file)
|
||||
# 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
|
||||
|
||||
# TODO: Dependencies
|
||||
end
|
||||
|
||||
files.each do |file|
|
||||
@files.each do |file|
|
||||
next if packages.detect do |pkg|
|
||||
pkg.category == "games" &&
|
||||
pkg.subcategory == @app_id &&
|
||||
pkg.name == file.package &&
|
||||
pkg.name.to_s.casecmp?(file.package.to_s) &&
|
||||
pkg.version == file.version
|
||||
end
|
||||
|
||||
@@ -263,11 +303,10 @@ class W3DHub
|
||||
packages.push(package)
|
||||
end
|
||||
|
||||
|
||||
packages
|
||||
@packages = packages
|
||||
end
|
||||
|
||||
def verify_files(manifests, packages)
|
||||
def verify_files
|
||||
@status.operations.clear
|
||||
@status.label = "Downloading #{@application.name}..."
|
||||
@status.value = "Verifying installed files..."
|
||||
@@ -284,46 +323,42 @@ class W3DHub
|
||||
|
||||
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
|
||||
|
||||
manifest.files.each do |file|
|
||||
safe_file_name = file.name.gsub("\\", "/")
|
||||
# Fix borked Data -> data 'cause Windows don't care about capitalization
|
||||
safe_file_name.sub!("Data/", "data/")
|
||||
file_path = normalize_path(file.name, path)
|
||||
|
||||
file_path = "#{path}/#{safe_file_name}"
|
||||
processed_files += 1
|
||||
@status.progress = processed_files.to_f / file_count
|
||||
|
||||
processed_files += 1
|
||||
@status.progress = processed_files.to_f / file_count
|
||||
next if file.removed_since
|
||||
next if accepted_files.key?(file_path)
|
||||
|
||||
next if file.removed_since
|
||||
next if accepted_files.key?(safe_file_name)
|
||||
unless File.exist?(file_path)
|
||||
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)
|
||||
rejected_files << { file: file, manifest_version: manifest.version }
|
||||
logger.info(LOG_TAG) { "[#{manifest.version}] File missing: #{file_path}" }
|
||||
next
|
||||
end
|
||||
digest = Digest::SHA256.new
|
||||
f = File.open(file_path)
|
||||
|
||||
digest = Digest::SHA256.new
|
||||
f = File.open(file_path)
|
||||
while (chunk = f.read(32_000_000))
|
||||
digest.update(chunk)
|
||||
end
|
||||
|
||||
while (chunk = f.read(32_000_000))
|
||||
digest.update(chunk)
|
||||
end
|
||||
f.close
|
||||
|
||||
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[safe_file_name] = manifest.version
|
||||
logger.info(LOG_TAG) { "[#{manifest.version}] Verified file: #{file_path}" }
|
||||
else
|
||||
rejected_files << { file: file, manifest_version: manifest.version }
|
||||
logger.info(LOG_TAG) { "[#{manifest.version}] File failed Verification: #{file_path}" }
|
||||
end
|
||||
if digest.hexdigest.upcase == file.checksum.upcase
|
||||
accepted_files[file_path] = file.version
|
||||
logger.info(LOG_TAG) { "[#{file.version}] Verified file: #{file_path}" }
|
||||
else
|
||||
rejected_files << { file: file, manifest_version: file.version }
|
||||
logger.info(LOG_TAG) { "[#{file.version}] File failed Verification: #{file_path}" }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -332,11 +367,10 @@ class W3DHub
|
||||
selected_packages = []
|
||||
selected_packages_hash = {}
|
||||
|
||||
# FIXME: Refactoring `build_package_list` has broken this bit since we no longer fetch EVERYTHING and actually DON'T download REMOVED files
|
||||
rejected_files.each do |hash|
|
||||
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
|
||||
selected_packages_hash["#{hash[:file].package}_#{hash[:manifest_version]}"] = true
|
||||
@@ -347,13 +381,15 @@ class W3DHub
|
||||
end
|
||||
|
||||
# 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
|
||||
|
||||
def fetch_packages(packages)
|
||||
hashes = packages.map do |pkg|
|
||||
def fetch_packages
|
||||
return if @packages.empty?
|
||||
|
||||
hashes = @packages.map do |pkg|
|
||||
{
|
||||
category: pkg.category,
|
||||
subcategory: pkg.subcategory,
|
||||
@@ -362,98 +398,103 @@ class W3DHub
|
||||
}
|
||||
end
|
||||
|
||||
package_details = Api.package_details(hashes)
|
||||
package_details = Api.package_details(hashes, @channel.source || :w3dhub)
|
||||
|
||||
if 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
|
||||
unless package_details
|
||||
fail!("Failed to fetch package details")
|
||||
return
|
||||
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
|
||||
|
||||
def verify_packages(packages)
|
||||
def verify_packages
|
||||
end
|
||||
|
||||
def unpack_packages(packages)
|
||||
def unpack_packages
|
||||
path = Cache.install_path(@application, @channel)
|
||||
logger.info(LOG_TAG) { "Unpacking packages in '#{path}'..." }
|
||||
Cache.create_directories(path, true)
|
||||
@@ -463,7 +504,7 @@ class W3DHub
|
||||
@status.value = "Unpacking..."
|
||||
@status.progress = 0.0
|
||||
|
||||
packages.each do |pkg|
|
||||
@packages.each do |pkg|
|
||||
# FIXME: can't add a new key into hash during iteration (RuntimeError)
|
||||
@status.operations[:"#{pkg.checksum}"] = Status::Operation.new(
|
||||
label: pkg.name,
|
||||
@@ -475,7 +516,7 @@ class W3DHub
|
||||
@status.step = :unpacking
|
||||
|
||||
i = -1
|
||||
packages.each do |package|
|
||||
@packages.each do |package|
|
||||
i += 1
|
||||
|
||||
status = if package.custom_is_patch
|
||||
@@ -508,6 +549,23 @@ class W3DHub
|
||||
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
|
||||
if W3DHub.unix? && @wine_prefix
|
||||
# TODO: create a wine prefix if configured
|
||||
@@ -520,7 +578,7 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
def install_dependencies(packages)
|
||||
def install_dependencies
|
||||
# TODO: install dependencies
|
||||
@status.operations.clear
|
||||
@status.label = "Installing #{@application.name}..."
|
||||
@@ -530,6 +588,22 @@ class W3DHub
|
||||
@status.step = :install_dependencies
|
||||
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
|
||||
Store.application_manager.installed!(self)
|
||||
|
||||
@@ -551,12 +625,17 @@ class W3DHub
|
||||
# Check for and integrity of local manifest
|
||||
|
||||
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)
|
||||
package = array.first
|
||||
else
|
||||
fail!("Failed to fetch manifest package details!")
|
||||
return
|
||||
fail!("Failed to fetch manifest package details! (#{category}:#{subcategory}:#{name}:#{version})")
|
||||
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
|
||||
|
||||
if File.exist?(Cache.package_path(category, subcategory, name, version))
|
||||
@@ -574,9 +653,13 @@ class W3DHub
|
||||
def package_fetch(package, &block)
|
||||
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)
|
||||
end
|
||||
|
||||
fail!("Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})") unless status_okay
|
||||
|
||||
status_okay
|
||||
end
|
||||
|
||||
def verify_package(package, &block)
|
||||
@@ -588,7 +671,7 @@ class W3DHub
|
||||
return false unless File.exist?(path)
|
||||
|
||||
operation = @status.operations[:"#{package.checksum}"]
|
||||
operation&.value = "Verifying..."
|
||||
operation&.value = "Verifying..."
|
||||
|
||||
file_size = File.size(path)
|
||||
logger.info(LOG_TAG) { " File size: #{file_size}" }
|
||||
@@ -653,38 +736,52 @@ class W3DHub
|
||||
logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" }
|
||||
unzip(package_path, temp_path)
|
||||
|
||||
# Fix borked Data -> data 'cause Windows don't care about capitalization
|
||||
safe_file_name = "#{manifest_file.name.sub('Data/', 'data/')}"
|
||||
file_path = normalize_path(manifest_file.name, path)
|
||||
temp_file_path = normalize_path(manifest_file.name, temp_path)
|
||||
|
||||
logger.info(LOG_TAG) { " Loading #{temp_path}/#{safe_file_name}.patch..." }
|
||||
patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_path}/#{safe_file_name}.patch", ignore_crc_mismatches: false)
|
||||
patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name == ".w3dhub.patch" || f.name == ".bhppatch" }.data, symbolize_names: true)
|
||||
logger.info(LOG_TAG) { " Loading #{temp_file_path}.patch..." }
|
||||
patch_mix = W3DHub::WWMix.new(path: "#{temp_file_path}.patch")
|
||||
unless patch_mix.load
|
||||
raise patch_mix.error_reason
|
||||
end
|
||||
patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") }
|
||||
patch_entry.read
|
||||
# "remove" patch meta file from patch before copying patch data
|
||||
patch_mix.entries.delete(patch_entry)
|
||||
|
||||
logger.info(LOG_TAG) { " Loading #{path}/#{safe_file_name}..." }
|
||||
target_mix = W3DHub::Mixer::Reader.new(file_path: "#{path}/#{safe_file_name}", ignore_crc_mismatches: false)
|
||||
patch_info = JSON.parse(patch_entry.blob, symbolize_names: true)
|
||||
|
||||
logger.info(LOG_TAG) { " Loading #{file_path}..." }
|
||||
target_mix = W3DHub::WWMix.new(path: "#{file_path}")
|
||||
unless target_mix.load
|
||||
raise target_mix.error_reason
|
||||
end
|
||||
|
||||
logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive?
|
||||
patch_info[:removedFiles].each do |file|
|
||||
logger.debug(LOG_TAG) { " #{file}" }
|
||||
target_mix.package.files.delete_if { |f| f.name == file }
|
||||
target_mix.entries.delete_if { |e| e.name.casecmp?(file) }
|
||||
end
|
||||
|
||||
logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive?
|
||||
patch_info[:updatedFiles].each do |file|
|
||||
logger.debug(LOG_TAG) { " #{file}" }
|
||||
|
||||
patch = patch_mix.package.files.find { |f| f.name == file }
|
||||
target = target_mix.package.files.find { |f| f.name == file }
|
||||
|
||||
if target
|
||||
target_mix.package.files[target_mix.package.files.index(target)] = patch
|
||||
else
|
||||
target_mix.package.files << patch
|
||||
patch_mix.entries.each do |entry|
|
||||
target_mix.add_entry(entry: entry, replace: true)
|
||||
end
|
||||
end
|
||||
|
||||
logger.info(LOG_TAG) { " Writing updated #{path}/#{safe_file_name}..." } if patch_info[:updatedFiles].size.positive?
|
||||
W3DHub::Mixer::Writer.new(file_path: "#{path}/#{safe_file_name}", package: target_mix.package, memory_buffer: true)
|
||||
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
|
||||
temp_mix_path = "#{temp_path}/#{File.basename(file_path)}"
|
||||
temp_mix = W3DHub::WWMix.new(path: temp_mix_path, encrypted: target_mix.encrypted?)
|
||||
target_mix.entries.each { |e| temp_mix.add_entry(entry: e, replace: true) }
|
||||
unless temp_mix.save
|
||||
raise temp_mix.error_reason
|
||||
end
|
||||
|
||||
# Overwrite target mix with temp mix
|
||||
FileUtils.mv(temp_mix_path, file_path)
|
||||
|
||||
FileUtils.remove_dir(temp_path)
|
||||
|
||||
@@ -695,17 +792,15 @@ class W3DHub
|
||||
stream = Zip::InputStream.new(File.open(package_path))
|
||||
|
||||
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("\\", "/")
|
||||
# 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)}"
|
||||
dir_path = File.dirname(file_path)
|
||||
unless dir_path.end_with?("/.") || Dir.exist?(dir_path)
|
||||
FileUtils.mkdir_p(dir_path)
|
||||
end
|
||||
|
||||
File.open("#{path}/#{safe_file_name}", "wb") do |f|
|
||||
File.open(file_path, "wb") do |f|
|
||||
i = entry.get_input_stream
|
||||
|
||||
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
|
||||
return false if failed?
|
||||
|
||||
manifests = fetch_manifests
|
||||
fetch_manifests
|
||||
return false if failed?
|
||||
|
||||
packages = build_package_list(manifests)
|
||||
build_package_list
|
||||
return false if failed?
|
||||
|
||||
verify_files(manifests, packages)
|
||||
remove_deleted_files
|
||||
return false if failed?
|
||||
|
||||
fetch_packages(packages)
|
||||
verify_files
|
||||
return false if failed?
|
||||
|
||||
verify_packages(packages)
|
||||
fetch_packages
|
||||
return false if failed?
|
||||
|
||||
unpack_packages(packages)
|
||||
verify_packages
|
||||
return false if failed?
|
||||
|
||||
unpack_packages
|
||||
return false if failed?
|
||||
|
||||
create_wine_prefix
|
||||
return false if failed?
|
||||
|
||||
install_dependencies(packages)
|
||||
install_dependencies
|
||||
return false if failed?
|
||||
|
||||
write_paths_ini
|
||||
return false if failed?
|
||||
|
||||
mark_application_installed
|
||||
@@ -47,4 +53,4 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,24 +6,6 @@ class W3DHub
|
||||
def type
|
||||
:repairer
|
||||
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
|
||||
@@ -76,7 +76,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
def save_config(config = @config)
|
||||
File.write(CONFIG_PATH, config.to_json)
|
||||
File.write(CONFIG_PATH, JSON.pretty_generate(config))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,7 +23,12 @@ class W3DHub
|
||||
def self.verify_peer
|
||||
no_verify.tap do |context|
|
||||
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
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ class W3DHub
|
||||
|
||||
flow(width: 1.0, fill: true) do
|
||||
@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
|
||||
@game_path.value = path if !path.empty? && File.exist?(path)
|
||||
end
|
||||
|
||||
@@ -43,20 +43,20 @@ class W3DHub
|
||||
@@instance.kill!
|
||||
end
|
||||
|
||||
def self.job(job, callback, error_handler = nil)
|
||||
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler))
|
||||
def self.job(job, callback, error_handler = nil, data = nil)
|
||||
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, data: data))
|
||||
end
|
||||
|
||||
def self.parallel_job(job, callback, error_handler = nil)
|
||||
@@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler))
|
||||
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, data: data))
|
||||
end
|
||||
|
||||
def self.foreground_job(job, callback, error_handler = nil)
|
||||
@@instance.add_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true))
|
||||
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, data: data))
|
||||
end
|
||||
|
||||
def self.foreground_parallel_job(job, callback, error_handler = nil)
|
||||
@@instance.add_parallel_job(Job.new(job: job, callback: callback, error_handler: error_handler, deliver_to_queue: true))
|
||||
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, data: data))
|
||||
end
|
||||
|
||||
def initialize
|
||||
@@ -136,16 +136,16 @@ class W3DHub
|
||||
end
|
||||
|
||||
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
|
||||
@callback = callback
|
||||
@error_handler = error_handler
|
||||
|
||||
@deliver_to_queue = deliver_to_queue
|
||||
@data = data
|
||||
end
|
||||
|
||||
def do
|
||||
result = @job.call
|
||||
result = @data ? @job.call(@data) : @job.call
|
||||
deliver(result)
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class W3DHub
|
||||
# Maybe add remote game launch from server list app?
|
||||
class MulticastServer
|
||||
MULTICAST_ADDR = "224.87.51.68"
|
||||
# Maybe add intranet package delivery?
|
||||
class BroadcastServer
|
||||
PORT = 7050
|
||||
|
||||
def initialize
|
||||
107
lib/cache.rb
@@ -9,18 +9,18 @@ class W3DHub
|
||||
end
|
||||
|
||||
# Fetch a generic uri
|
||||
def self.fetch(uri:, force_fetch: false, async: true)
|
||||
def self.fetch(uri:, force_fetch: false, async: true, backend: :w3dhub)
|
||||
path = path(uri)
|
||||
|
||||
if !force_fetch && File.exist?(path)
|
||||
path
|
||||
elsif async
|
||||
BackgroundWorker.job(
|
||||
-> { Api.get(uri, W3DHub::Api::DEFAULT_HEADERS) },
|
||||
-> { Api.fetch(uri, W3DHub::Api::DEFAULT_HEADERS, nil, backend) },
|
||||
->(response) { File.open(path, "wb") { |f| f.write response.body } if response.status == 200 }
|
||||
)
|
||||
else
|
||||
response = Api.get(uri, W3DHub::Api::DEFAULT_HEADERS)
|
||||
response = Api.fetch(uri, W3DHub::Api::DEFAULT_HEADERS, nil, backend)
|
||||
File.open(path, "wb") { |f| f.write response.body } if response.status == 200
|
||||
end
|
||||
end
|
||||
@@ -50,10 +50,16 @@ class W3DHub
|
||||
end
|
||||
|
||||
# Download a W3D Hub package
|
||||
# TODO: More work needed to make this work reliably
|
||||
def self._async_fetch_package(package, block)
|
||||
def self.async_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)
|
||||
headers = Api::FORM_ENCODED_HEADERS
|
||||
headers = [["content-type", "application/x-www-form-urlencoded"], ["user-agent", Api::USER_AGENT]]
|
||||
headers << ["authorization", "Bearer #{Store.account.access_token}"] if Store.account && !package.download_url
|
||||
body = URI.encode_www_form("data": JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version }))
|
||||
start_from_bytes = package.custom_partially_valid_at_bytes
|
||||
|
||||
logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" }
|
||||
@@ -63,67 +69,62 @@ class W3DHub
|
||||
file = File.open(path, start_from_bytes.positive? ? "r+b" : "wb")
|
||||
|
||||
if start_from_bytes.positive?
|
||||
headers = Api::FORM_ENCODED_HEADERS + [["Range", "bytes=#{start_from_bytes}-"]]
|
||||
headers << ["range", "bytes=#{start_from_bytes}-"]
|
||||
file.pos = start_from_bytes
|
||||
end
|
||||
|
||||
body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}"
|
||||
result = false
|
||||
Sync do
|
||||
uri = URI(endpoint_download_url)
|
||||
|
||||
response = Api.post("#{Api::ENDPOINT}/apis/launcher/1/get-package", headers, body)
|
||||
response = W3DHub::Api.provision_http_client(uri.origin).send((package.download_url ? :get : :post), uri.request_uri, headers, body)
|
||||
if response.success?
|
||||
total_bytes = package.size
|
||||
|
||||
total_bytes = package.size
|
||||
remaining_bytes = total_bytes - start_from_bytes
|
||||
response.each do |chunk|
|
||||
file.write(chunk)
|
||||
|
||||
response.each do |chunk|
|
||||
file.write(chunk)
|
||||
block.call(chunk, total_bytes - file.pos, total_bytes)
|
||||
end
|
||||
|
||||
remaining_bytes -= chunk.size
|
||||
result = true
|
||||
end
|
||||
|
||||
block.call(chunk, remaining_bytes, total_bytes)
|
||||
binding.irb unless response
|
||||
|
||||
if response&.status == 200 || response&.status == 206
|
||||
result = 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}" }
|
||||
|
||||
result = false
|
||||
end
|
||||
rescue Async::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}" }
|
||||
|
||||
result = false
|
||||
rescue StandardError => 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}" }
|
||||
|
||||
result = false
|
||||
ensure
|
||||
file&.close
|
||||
response&.close
|
||||
end
|
||||
|
||||
response.status == 200
|
||||
ensure
|
||||
file&.close
|
||||
result
|
||||
end
|
||||
|
||||
# Download a W3D Hub package
|
||||
def self.fetch_package(package, block)
|
||||
path = package_path(package.category, package.subcategory, package.name, package.version)
|
||||
headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT }
|
||||
headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account
|
||||
start_from_bytes = package.custom_partially_valid_at_bytes
|
||||
|
||||
logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" }
|
||||
|
||||
create_directories(path)
|
||||
|
||||
file = File.open(path, start_from_bytes.positive? ? "r+b" : "wb")
|
||||
|
||||
if start_from_bytes.positive?
|
||||
headers["Range"] = "bytes=#{start_from_bytes}-"
|
||||
file.pos = start_from_bytes
|
||||
end
|
||||
|
||||
streamer = lambda do |chunk, remaining_bytes, total_bytes|
|
||||
file.write(chunk)
|
||||
|
||||
block.call(chunk, remaining_bytes, total_bytes)
|
||||
end
|
||||
|
||||
# Create a new connection due to some weirdness somewhere in Excon
|
||||
response = Excon.post(
|
||||
"#{Api::ENDPOINT}/apis/launcher/1/get-package",
|
||||
tcp_nodelay: true,
|
||||
headers: headers,
|
||||
body: "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}",
|
||||
chunk_size: 50_000,
|
||||
response_block: streamer
|
||||
)
|
||||
|
||||
response.status == 200 || response.status == 206
|
||||
ensure
|
||||
file&.close
|
||||
async_fetch_package(package, block)
|
||||
end
|
||||
|
||||
def self.acquire_net_lock(key)
|
||||
|
||||
157
lib/common.rb
@@ -32,6 +32,15 @@ class W3DHub
|
||||
linux? || mac?
|
||||
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)
|
||||
raise "Hazardous input: #{path}" if path.include?("&&") || path.include?(";")
|
||||
|
||||
@@ -45,47 +54,49 @@ class W3DHub
|
||||
end
|
||||
|
||||
def self.prompt_for_nickname(accept_callback: nil, cancel_callback: nil)
|
||||
CyberarmEngine::Window.instance.push_state(
|
||||
W3DHub::States::PromptDialog,
|
||||
title: I18n.t(:"server_browser.set_nickname"),
|
||||
message: I18n.t(:"server_browser.set_nickname_message"),
|
||||
prefill: Store.settings[:server_list_username],
|
||||
accept_callback: accept_callback,
|
||||
cancel_callback: cancel_callback,
|
||||
# See: https://gitlab.com/danpaul88/brenbot/-/blob/master/Source/renlog.pm#L136-175
|
||||
valid_callback: proc do |entry|
|
||||
entry.length > 1 && entry.length < 30 && (entry =~ /(:|!|&|%| )/i).nil? &&
|
||||
(entry =~ /[\001\002\037]/).nil? && (entry =~ /\\/).nil?
|
||||
end
|
||||
)
|
||||
CyberarmEngine::Window.instance.push_state(
|
||||
W3DHub::States::PromptDialog,
|
||||
title: I18n.t(:"server_browser.set_nickname"),
|
||||
message: I18n.t(:"server_browser.set_nickname_message"),
|
||||
prefill: Store.settings[:server_list_username],
|
||||
accept_callback: accept_callback,
|
||||
cancel_callback: cancel_callback,
|
||||
valid_callback: proc do |entry|
|
||||
entry.length.between?(3, 40) && (entry =~ /^[a-z0-9_\-\[\]]+$/i)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def self.prompt_for_password(accept_callback: nil, cancel_callback: nil)
|
||||
CyberarmEngine::Window.instance.push_state(
|
||||
W3DHub::States::PromptDialog,
|
||||
title: I18n.t(:"server_browser.enter_password"),
|
||||
message: I18n.t(:"server_browser.enter_password_message"),
|
||||
input_type: :password,
|
||||
accept_callback: accept_callback,
|
||||
cancel_callback: cancel_callback,
|
||||
valid_callback: proc { |entry| entry.length.positive? }
|
||||
)
|
||||
end
|
||||
def self.prompt_for_password(accept_callback: nil, cancel_callback: nil)
|
||||
CyberarmEngine::Window.instance.push_state(
|
||||
W3DHub::States::PromptDialog,
|
||||
title: I18n.t(:"server_browser.enter_password"),
|
||||
message: I18n.t(:"server_browser.enter_password_message"),
|
||||
input_type: :password,
|
||||
accept_callback: accept_callback,
|
||||
cancel_callback: cancel_callback,
|
||||
valid_callback: proc { |entry| entry.length.positive? }
|
||||
)
|
||||
end
|
||||
|
||||
def self.join_server(server, password)
|
||||
if (
|
||||
(server.status.password && password.length.positive?) ||
|
||||
!server.status.password) &&
|
||||
Store.settings[:server_list_username].to_s.length.positive?
|
||||
def self.join_server(server:, username: Store.settings[:server_list_username], password: nil, multi: false)
|
||||
if (
|
||||
(server.status.password && password.length.positive?) ||
|
||||
!server.status.password) &&
|
||||
username.to_s.length.positive?
|
||||
|
||||
Store.application_manager.join_server(
|
||||
server.game,
|
||||
server.channel, server, password
|
||||
)
|
||||
else
|
||||
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
|
||||
end
|
||||
end
|
||||
Store.application_manager.join_server(
|
||||
server.game,
|
||||
server.channel,
|
||||
server,
|
||||
username,
|
||||
password,
|
||||
multi
|
||||
)
|
||||
else
|
||||
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
|
||||
end
|
||||
end
|
||||
|
||||
def self.command(command, &block)
|
||||
if windows?
|
||||
@@ -110,7 +121,6 @@ class W3DHub
|
||||
process_info = Process.create(**hash)
|
||||
|
||||
pid = process_info.process_id
|
||||
status = -1
|
||||
|
||||
until (status = Process.get_exitcode(pid))
|
||||
if block
|
||||
@@ -127,18 +137,16 @@ class W3DHub
|
||||
end
|
||||
|
||||
status.zero?
|
||||
else
|
||||
if block
|
||||
IO.popen(command, "r") do |io|
|
||||
io.each_line do |line|
|
||||
block&.call(line)
|
||||
end
|
||||
elsif block
|
||||
IO.popen(command, "r") do |io|
|
||||
io.each_line do |line|
|
||||
block&.call(line)
|
||||
end
|
||||
|
||||
$CHILD_STATUS.success?
|
||||
else
|
||||
system(command)
|
||||
end
|
||||
|
||||
$CHILD_STATUS.success?
|
||||
else
|
||||
system(command)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -146,27 +154,60 @@ class W3DHub
|
||||
File.expand_path("~")
|
||||
end
|
||||
|
||||
def self.ask_file(title: "Open File", filter: "*game*.exe")
|
||||
def self.ask_file(title: "Open File", filter: "*game*.exe", filters: [])
|
||||
filters << filter if filters.empty?
|
||||
|
||||
if W3DHub.unix?
|
||||
# search for command
|
||||
cmds = %w{ zenity matedialog qarma kdialog }
|
||||
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 --title "#{title}" --file-filter "#{filter}"`
|
||||
when "kdialog"
|
||||
`#{command} --title "#{title}" --getopenfilename . "#{filter}"`
|
||||
else
|
||||
raise "No known command found for system file selection dialog!"
|
||||
end
|
||||
when "zenity", "matedialog", "qarma"
|
||||
options = filters.map { |s| format("--file-filter=\"%s\"", s) }.join(" ")
|
||||
`#{command} --file-selection --title \"#{title}\" #{options}`
|
||||
when "kdialog"
|
||||
`#{command} --title "#{title}" --getopenfilename . "#{filters.join(" ")}"`
|
||||
else
|
||||
raise "No known command found for system file selection dialog!"
|
||||
end
|
||||
|
||||
path.strip
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -2,7 +2,7 @@ class W3DHub
|
||||
class HardwareSurvey
|
||||
attr_reader :data
|
||||
|
||||
def initialize
|
||||
def initialize(displays_only: false)
|
||||
@data = {
|
||||
displays: [],
|
||||
system: {
|
||||
@@ -26,8 +26,6 @@ class W3DHub
|
||||
}
|
||||
}
|
||||
|
||||
# Hardware survey only works on Windows atm
|
||||
|
||||
if Gem::win_platform?
|
||||
lib_dir = File.dirname($LOADED_FEATURES.find { |file| file.include?("gosu.so") })
|
||||
SDL.load_lib("#{lib_dir}64/SDL2.dll")
|
||||
@@ -36,11 +34,13 @@ class W3DHub
|
||||
end
|
||||
|
||||
query_displays
|
||||
query_motherboard
|
||||
query_operating_system
|
||||
query_cpus
|
||||
query_ram
|
||||
query_gpus
|
||||
unless displays_only
|
||||
query_motherboard
|
||||
query_operating_system
|
||||
query_cpus
|
||||
query_ram
|
||||
query_gpus
|
||||
end
|
||||
|
||||
@data.freeze
|
||||
end
|
||||
@@ -68,37 +68,51 @@ class W3DHub
|
||||
end
|
||||
|
||||
def query_motherboard
|
||||
return unless Gem::win_platform?
|
||||
|
||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("HARDWARE\\DESCRIPTION\\System\\BIOS", Win32::Registry::KEY_READ) do |reg|
|
||||
@data[:system][:motherboard][:manufacturer] = safe_reg(reg, "SystemManufacturer")
|
||||
@data[:system][:motherboard][:model] = safe_reg(reg, "SystemProductName")
|
||||
@data[:system][:motherboard][:bios_vendor] = safe_reg(reg, "BIOSVendor")
|
||||
@data[:system][:motherboard][:bios_release_date] = safe_reg(reg, "BIOSReleaseDate")
|
||||
@data[:system][:motherboard][:bios_version] = safe_reg(reg, "BIOSVersion")
|
||||
if Gem::win_platform?
|
||||
begin
|
||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("HARDWARE\\DESCRIPTION\\System\\BIOS", Win32::Registry::KEY_READ) do |reg|
|
||||
@data[:system][:motherboard][:manufacturer] = safe_reg(reg, "SystemManufacturer")
|
||||
@data[:system][:motherboard][:model] = safe_reg(reg, "SystemProductName")
|
||||
@data[:system][:motherboard][:bios_vendor] = safe_reg(reg, "BIOSVendor")
|
||||
@data[:system][:motherboard][:bios_release_date] = safe_reg(reg, "BIOSReleaseDate")
|
||||
@data[:system][:motherboard][:bios_version] = safe_reg(reg, "BIOSVersion")
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
@data[:system][:motherboard][:manufacturer] = "Unknown"
|
||||
@data[:system][:motherboard][:model] = "Unknown"
|
||||
@data[:system][:motherboard][:bios_vendor] = "Unknown"
|
||||
@data[:system][:motherboard][:bios_release_date] = "Unknown"
|
||||
@data[:system][:motherboard][:bios_version] = "Unknown"
|
||||
end
|
||||
else # unix
|
||||
@data[:system][:motherboard][:manufacturer] = safe_file("/sys/devices/virtual/dmi/id/board_vendor")
|
||||
@data[:system][:motherboard][:model] = safe_file("/sys/devices/virtual/dmi/id/board_name")
|
||||
@data[:system][:motherboard][:bios_version] = safe_file("/sys/devices/virtual/dmi/id/board_version")
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
@data[:system][:motherboard][:manufacturer] = "Unknown"
|
||||
@data[:system][:motherboard][:model] = "Unknown"
|
||||
@data[:system][:motherboard][:bios_vendor] = "Unknown"
|
||||
@data[:system][:motherboard][:bios_release_date] = "Unknown"
|
||||
@data[:system][:motherboard][:bios_version] = "Unknown"
|
||||
end
|
||||
|
||||
def query_operating_system
|
||||
return unless Gem::win_platform?
|
||||
|
||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Win32::Registry::KEY_READ) do |reg|
|
||||
@data[:system][:operating_system][:name] = safe_reg(reg, "ProductName")
|
||||
@data[:system][:operating_system][:build] = safe_reg(reg, "CurrentBuild")
|
||||
@data[:system][:operating_system][:version] = safe_reg(reg, "DisplayVersion")
|
||||
@data[:system][:operating_system][:edition] = safe_reg(reg, "EditionID")
|
||||
if Gem::win_platform?
|
||||
begin
|
||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Win32::Registry::KEY_READ) do |reg|
|
||||
@data[:system][:operating_system][:name] = safe_reg(reg, "ProductName")
|
||||
@data[:system][:operating_system][:build] = safe_reg(reg, "CurrentBuild")
|
||||
@data[:system][:operating_system][:version] = safe_reg(reg, "DisplayVersion")
|
||||
@data[:system][:operating_system][:edition] = safe_reg(reg, "EditionID")
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
@data[:system][:operating_system][:name] = "Unknown"
|
||||
@data[:system][:operating_system][:build] = "Unknown"
|
||||
@data[:system][:operating_system][:version] = "Unknown"
|
||||
@data[:system][:operating_system][:edition] = "Unknown"
|
||||
end
|
||||
else # unix
|
||||
release_info = query_release_info
|
||||
@data[:system][:operating_system][:name] = release_info["pretty_name"] || release_info["name"] || "Unknown"
|
||||
@data[:system][:operating_system][:build] = release_info["version_codename"] || release_info["build_id"] || "Unknown"
|
||||
@data[:system][:operating_system][:version] = release_info["version_id"] || release_info["build_id"] || "Unknown"
|
||||
@data[:system][:operating_system][:edition] = release_info["id"] || release_info["id_like"] || "Unknown"
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
@data[:system][:operating_system][:name] = "Unknown"
|
||||
@data[:system][:operating_system][:build] = "Unknown"
|
||||
@data[:system][:operating_system][:version] = "Unknown"
|
||||
@data[:system][:operating_system][:edition] = "Unknown"
|
||||
end
|
||||
|
||||
def query_cpus
|
||||
@@ -122,6 +136,16 @@ class W3DHub
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
end
|
||||
else
|
||||
cpu_info = query_cpu_info
|
||||
cpu_info.each do |cpu|
|
||||
@data[:system][:cpus] << {
|
||||
manufacturer: cpu["manufacturer"] || "Unknown",
|
||||
model: cpu["model"] || "Unknown",
|
||||
mhz: cpu["mhz"] || "Unknown",
|
||||
family: cpu["family"] || "Unknown"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
instruction_sets = %w[ HasRDTSC HasAltiVec HasMMX Has3DNow HasSSE HasSSE2 HasSSE3 HasSSE41 HasSSE42 HasAVX HasAVX2 HasAVX512F HasARMSIMD HasNEON ] # HasLSX HasLASX # These cause a crash atm
|
||||
@@ -140,44 +164,57 @@ class W3DHub
|
||||
end
|
||||
|
||||
def query_gpus
|
||||
return unless Gem::win_platform?
|
||||
if Gem::win_platform?
|
||||
begin
|
||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}", Win32::Registry::KEY_READ) do |reg|
|
||||
i = 0
|
||||
|
||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}", Win32::Registry::KEY_READ) do |reg|
|
||||
i = 0
|
||||
reg.each_key do |key, _|
|
||||
next unless key.start_with?("0")
|
||||
|
||||
reg.each_key do |key, _|
|
||||
next unless key.start_with?("0")
|
||||
|
||||
reg.open(key) do |device|
|
||||
vram = -1
|
||||
|
||||
begin
|
||||
vram = device["HardwareInformation.qwMemorySize"].to_i
|
||||
rescue Win32::Registry::Error, TypeError
|
||||
begin
|
||||
vram = device["HardwareInformation.MemorySize"].to_i
|
||||
rescue Win32::Registry::Error, TypeError
|
||||
reg.open(key) do |device|
|
||||
vram = -1
|
||||
|
||||
begin
|
||||
vram = device["HardwareInformation.qwMemorySize"].to_i
|
||||
rescue Win32::Registry::Error, TypeError
|
||||
begin
|
||||
vram = device["HardwareInformation.MemorySize"].to_i
|
||||
rescue Win32::Registry::Error, TypeError
|
||||
vram = -1
|
||||
end
|
||||
end
|
||||
|
||||
next if vram.negative?
|
||||
|
||||
vram = vram / 1024.0 / 1024.0
|
||||
|
||||
@data[:system][:gpus] << {
|
||||
manufacturer: safe_reg(device, "ProviderName"),
|
||||
model: safe_reg(device, "DriverDesc"),
|
||||
vram: vram.round,
|
||||
driver_date: safe_reg(device, "DriverDate"),
|
||||
driver_version: safe_reg(device, "DriverVersion")
|
||||
}
|
||||
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
next if vram.negative?
|
||||
|
||||
vram = vram / 1024.0 / 1024.0
|
||||
|
||||
@data[:system][:gpus] << {
|
||||
manufacturer: safe_reg(device, "ProviderName"),
|
||||
model: safe_reg(device, "DriverDesc"),
|
||||
vram: vram.round,
|
||||
driver_date: safe_reg(device, "DriverDate"),
|
||||
driver_version: safe_reg(device, "DriverVersion")
|
||||
}
|
||||
|
||||
i += 1
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
end
|
||||
else # unix
|
||||
gpu_info = query_glx_info
|
||||
gpu_info.each do |gpu|
|
||||
@data[:system][:gpus] << {
|
||||
manufacturer: gpu["manufacturer"] || "Unknown",
|
||||
model: gpu["model"] || "Unknown",
|
||||
vram: gpu["vram"].to_i,
|
||||
driver_date: gpu["driver_date"] || "Unknown",
|
||||
driver_version: gpu["driver_version"] || "Unknown"
|
||||
}
|
||||
end
|
||||
end
|
||||
rescue Win32::Registry::Error
|
||||
end
|
||||
|
||||
def safe_reg(reg, key, default_value = "Unknown")
|
||||
@@ -185,5 +222,130 @@ class W3DHub
|
||||
rescue Win32::Registry::Error
|
||||
default_value
|
||||
end
|
||||
|
||||
def safe_file(path, default_value = "Unknown")
|
||||
value = File.read(path).to_s.strip
|
||||
return default_value if value.downcase == "default string"
|
||||
|
||||
value
|
||||
rescue
|
||||
default_value
|
||||
end
|
||||
|
||||
def query_release_info
|
||||
hash = {}
|
||||
|
||||
File.open("/etc/os-release") do |f|
|
||||
f.each_line do |line|
|
||||
line = line.strip
|
||||
|
||||
key, value = line.split("=", 2)
|
||||
value.gsub!('"', "")
|
||||
|
||||
hash[key.downcase] = value
|
||||
end
|
||||
end
|
||||
|
||||
hash
|
||||
rescue
|
||||
hash
|
||||
end
|
||||
|
||||
def query_cpu_info
|
||||
cpus = []
|
||||
|
||||
cpu = {}
|
||||
File.open("/proc/cpuinfo") do |f|
|
||||
f.each_line do |line|
|
||||
line = line.strip
|
||||
|
||||
if line.empty?
|
||||
cpu["family"] = format(
|
||||
"%s Family %s Model %s Stepping %s",
|
||||
cpu["manufacturer"] || "Unknown",
|
||||
cpu["_family"] || "Unknown",
|
||||
cpu["_model"] || "Unknown",
|
||||
cpu["_stepping"] || "Unknown",
|
||||
)
|
||||
|
||||
cpus << cpu
|
||||
cpu = {}
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
key, value = line.split(":", 2).map(&:strip)
|
||||
|
||||
case key.downcase
|
||||
when "vendor_id"
|
||||
cpu["manufacturer"] = value
|
||||
when "model name"
|
||||
cpu["model"] = value
|
||||
when "cpu mhz"
|
||||
cpu["mhz"] = value
|
||||
|
||||
when "cpu family"
|
||||
cpu["_family"] = value
|
||||
when "model"
|
||||
cpu["_model"] = value
|
||||
when "stepping"
|
||||
cpu["_stepping"] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cpus
|
||||
rescue
|
||||
cpus
|
||||
end
|
||||
|
||||
def query_glx_info
|
||||
gpus = []
|
||||
glxinfo = `glxinfo`
|
||||
|
||||
return gpus if glxinfo.empty?
|
||||
|
||||
gpu = {}
|
||||
glxinfo.lines do |line|
|
||||
line = line.strip
|
||||
|
||||
next if line.empty?
|
||||
|
||||
key, value = line.split(":", 2).map(&:strip)
|
||||
|
||||
mesa_info = false
|
||||
gpu_memory_info = false
|
||||
case key.downcase
|
||||
when "opengl vendor string"
|
||||
if mesa_info
|
||||
gpus << gpu
|
||||
gpu = {}
|
||||
|
||||
break
|
||||
end
|
||||
when /extended renderer info \(GLX_MESA_query_renderer\)/i
|
||||
# Joy and happiness
|
||||
mesa_info = true
|
||||
when /Memory info \(GL_NVX_gpu_memory_info\)/i
|
||||
# Happiness and joy
|
||||
gpu_memory_info = true
|
||||
when "vendor", "opengl vendor string"
|
||||
gpu["manufacturer"] = value
|
||||
when "device", "opengl renderer string"
|
||||
gpu["model"] = value
|
||||
when "version"
|
||||
gpu["driver_version"] = value
|
||||
when "video memory", "dedicated video memory"
|
||||
gpu["vram"] = value.gsub(/[\D]+/, "")
|
||||
when "opengl version string"
|
||||
gpus << gpu
|
||||
gpu = {}
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
gpus
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
370
lib/mixer.rb
@@ -1,370 +0,0 @@
|
||||
require "digest"
|
||||
require "stringio"
|
||||
|
||||
class W3DHub
|
||||
|
||||
# https://github.com/TheUnstoppable/MixLibrary used for reference
|
||||
class Mixer
|
||||
DEFAULT_BUFFER_SIZE = 32_000_000
|
||||
|
||||
class MixParserException < RuntimeError; end
|
||||
class MixFormatException < RuntimeError; end
|
||||
|
||||
class MemoryBuffer
|
||||
def initialize(file_path:, mode:, buffer_size:, encoding: Encoding::ASCII_8BIT)
|
||||
@mode = mode
|
||||
|
||||
@file = File.open(file_path, mode == :read ? "rb" : "wb")
|
||||
@file.pos = 0
|
||||
@file_size = File.size(file_path)
|
||||
|
||||
@buffer_size = buffer_size
|
||||
@chunk = 0
|
||||
@last_chunk = 0
|
||||
@max_chunks = @file_size / @buffer_size
|
||||
@last_cached_chunk = nil
|
||||
|
||||
@encoding = encoding
|
||||
|
||||
@last_buffer_pos = 0
|
||||
@buffer = @mode == :read ? StringIO.new(@file.read(@buffer_size)) : StringIO.new
|
||||
@buffer.set_encoding(encoding)
|
||||
|
||||
# Cache frequently accessed chunks to reduce disk hits
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
def pos
|
||||
@chunk * @buffer_size + @buffer.pos
|
||||
end
|
||||
|
||||
def pos=(offset)
|
||||
last_chunk = @chunk
|
||||
@chunk = offset / @buffer_size
|
||||
|
||||
raise "No backsies! #{offset} (#{@chunk}/#{last_chunk})" if @mode == :write && @chunk < last_chunk
|
||||
|
||||
fetch_chunk(@chunk) if @mode == :read
|
||||
|
||||
@buffer.pos = offset % @buffer_size
|
||||
end
|
||||
|
||||
# string of bytes
|
||||
def write(bytes)
|
||||
length = bytes.length
|
||||
|
||||
# Crossing buffer boundry
|
||||
if @buffer.pos + length > @buffer_size
|
||||
|
||||
edge_size = @buffer_size - @buffer.pos
|
||||
buffer_edge = bytes[0...edge_size]
|
||||
|
||||
bytes_to_write = bytes.length - buffer_edge.length
|
||||
chunks_to_write = (bytes_to_write / @buffer_size.to_f).ceil
|
||||
bytes_written = buffer_edge.length
|
||||
|
||||
@buffer.write(buffer_edge)
|
||||
flush_chunk
|
||||
|
||||
chunks_to_write.times do |i|
|
||||
i += 1
|
||||
|
||||
@buffer.write(bytes[bytes_written...bytes_written + @buffer_size])
|
||||
bytes_written += @buffer_size
|
||||
|
||||
flush_chunk if string.length == @buffer_size
|
||||
end
|
||||
else
|
||||
@buffer.write(bytes)
|
||||
end
|
||||
|
||||
bytes
|
||||
end
|
||||
|
||||
def write_header(data_offset:, name_offset:)
|
||||
flush_chunk
|
||||
|
||||
@file.pos = 4
|
||||
write_i32(data_offset)
|
||||
write_i32(name_offset)
|
||||
|
||||
@file.pos = 0
|
||||
end
|
||||
|
||||
def write_i32(int)
|
||||
@file.write([int].pack("l"))
|
||||
end
|
||||
|
||||
def read(bytes = nil)
|
||||
raise ArgumentError, "Cannot read whole file" if bytes.nil?
|
||||
raise ArgumentError, "Cannot under read buffer" if bytes.negative?
|
||||
|
||||
# Long read, need to fetch next chunk while reading, mostly defeats this class...?
|
||||
if @buffer.pos + bytes > buffered
|
||||
buff = string[@buffer.pos..buffered]
|
||||
|
||||
bytes_to_read = bytes - buff.length
|
||||
chunks_to_read = (bytes_to_read / @buffer_size.to_f).ceil
|
||||
|
||||
chunks_to_read.times do |i|
|
||||
i += 1
|
||||
|
||||
fetch_chunk(@chunk + 1)
|
||||
|
||||
if i == chunks_to_read # read partial
|
||||
already_read_bytes = (chunks_to_read - 1) * @buffer_size
|
||||
bytes_more_to_read = bytes_to_read - already_read_bytes
|
||||
|
||||
buff << @buffer.read(bytes_more_to_read)
|
||||
else
|
||||
buff << @buffer.read
|
||||
end
|
||||
end
|
||||
|
||||
buff
|
||||
else
|
||||
fetch_chunk(@chunk) if @last_chunk != @chunk
|
||||
|
||||
@buffer.read(bytes)
|
||||
end
|
||||
end
|
||||
|
||||
def readbyte
|
||||
fetch_chunk(@chunk + 1) if @buffer.pos + 1 > buffered
|
||||
|
||||
@buffer.readbyte
|
||||
end
|
||||
|
||||
def fetch_chunk(chunk)
|
||||
raise ArgumentError, "Cannot fetch chunk #{chunk}, only #{@max_chunks} exist!" if chunk > @max_chunks
|
||||
@last_chunk = @chunk
|
||||
@chunk = chunk
|
||||
@last_buffer_pos = @buffer.pos
|
||||
|
||||
cached = @cache[chunk]
|
||||
|
||||
if cached
|
||||
@buffer.string = cached
|
||||
else
|
||||
@file.pos = chunk * @buffer_size
|
||||
buff = @buffer.string = @file.read(@buffer_size)
|
||||
|
||||
# Cache the active chunk (implementation bounces from @file_data_chunk and back to this for each 'file' processed)
|
||||
if @chunk != @file_data_chunk && @chunk != @last_cached_chunk
|
||||
@cache.delete(@last_cached_chunk) unless @last_cached_chunk == @file_data_chunk
|
||||
@cache[@chunk] = buff
|
||||
@last_cached_chunk = @chunk
|
||||
end
|
||||
|
||||
buff
|
||||
end
|
||||
end
|
||||
|
||||
# This is accessed quite often, keep it around
|
||||
def cache_file_data_chunk!
|
||||
@file_data_chunk = @chunk
|
||||
|
||||
last_buffer_pos = @buffer.pos
|
||||
@buffer.pos = 0
|
||||
@cache[@chunk] = @buffer.read
|
||||
@buffer.pos = last_buffer_pos
|
||||
end
|
||||
|
||||
def flush_chunk
|
||||
@last_chunk = @chunk
|
||||
@chunk += 1
|
||||
|
||||
@file.pos = @last_chunk * @buffer_size
|
||||
@file.write(string)
|
||||
|
||||
@buffer.string = "".force_encoding(@encoding)
|
||||
end
|
||||
|
||||
def string
|
||||
@buffer.string
|
||||
end
|
||||
|
||||
def buffered
|
||||
@buffer.string.length
|
||||
end
|
||||
|
||||
def close
|
||||
@file&.close
|
||||
end
|
||||
end
|
||||
|
||||
class Reader
|
||||
attr_reader :package
|
||||
|
||||
def initialize(file_path:, ignore_crc_mismatches: false, metadata_only: false, buffer_size: DEFAULT_BUFFER_SIZE)
|
||||
@package = Package.new
|
||||
|
||||
@buffer = MemoryBuffer.new(file_path: file_path, mode: :read, buffer_size: buffer_size)
|
||||
|
||||
@buffer.pos = 0
|
||||
|
||||
# Valid header
|
||||
if read_i32 == 0x3158494D
|
||||
file_data_offset = read_i32
|
||||
file_names_offset = read_i32
|
||||
|
||||
@buffer.pos = file_names_offset
|
||||
file_count = read_i32
|
||||
|
||||
file_count.times do
|
||||
@package.files << Package::File.new(name: read_string)
|
||||
end
|
||||
|
||||
@buffer.pos = file_data_offset
|
||||
@buffer.cache_file_data_chunk!
|
||||
|
||||
_file_count = read_i32
|
||||
|
||||
file_count.times do |i|
|
||||
file = @package.files[i]
|
||||
|
||||
file.mix_crc = read_u32.to_s(16).rjust(8, "0")
|
||||
file.content_offset = read_u32
|
||||
file.content_length = read_u32
|
||||
|
||||
if !ignore_crc_mismatches && file.mix_crc != file.file_crc
|
||||
raise MixParserException, "CRC mismatch for #{file.name}. #{file.mix_crc.inspect} != #{file.file_crc.inspect}"
|
||||
end
|
||||
|
||||
pos = @buffer.pos
|
||||
@buffer.pos = file.content_offset
|
||||
file.data = @buffer.read(file.content_length) unless metadata_only
|
||||
@buffer.pos = pos
|
||||
end
|
||||
else
|
||||
raise MixParserException, "Invalid MIX file"
|
||||
end
|
||||
|
||||
ensure
|
||||
@buffer&.close
|
||||
@buffer = nil # let GC collect
|
||||
end
|
||||
|
||||
def read_i32
|
||||
@buffer.read(4).unpack1("l")
|
||||
end
|
||||
|
||||
def read_u32
|
||||
@buffer.read(4).unpack1("L")
|
||||
end
|
||||
|
||||
def read_string
|
||||
buffer = ""
|
||||
|
||||
length = @buffer.readbyte
|
||||
|
||||
length.times do
|
||||
buffer << @buffer.readbyte
|
||||
end
|
||||
|
||||
buffer.strip
|
||||
end
|
||||
end
|
||||
|
||||
class Writer
|
||||
attr_reader :package
|
||||
|
||||
def initialize(file_path:, package:, memory_buffer: false, buffer_size: DEFAULT_BUFFER_SIZE)
|
||||
@package = package
|
||||
|
||||
@buffer = MemoryBuffer.new(file_path: file_path, mode: :write, buffer_size: buffer_size)
|
||||
@buffer.pos = 0
|
||||
|
||||
@buffer.write("MIX1")
|
||||
|
||||
files = @package.files.sort { |a, b| a.file_crc <=> b.file_crc }
|
||||
|
||||
@buffer.pos = 16
|
||||
|
||||
files.each do |file|
|
||||
file.content_offset = @buffer.pos
|
||||
file.content_length = file.data.length
|
||||
@buffer.write(file.data)
|
||||
|
||||
@buffer.pos += -@buffer.pos & 7
|
||||
end
|
||||
|
||||
file_data_offset = @buffer.pos
|
||||
write_i32(files.count)
|
||||
|
||||
files.each do |file|
|
||||
write_u32(file.file_crc.to_i(16))
|
||||
write_u32(file.content_offset)
|
||||
write_u32(file.content_length)
|
||||
end
|
||||
|
||||
file_name_offset = @buffer.pos
|
||||
write_i32(files.count)
|
||||
|
||||
files.each do |file|
|
||||
write_byte(file.name.length + 1)
|
||||
@buffer.write("#{file.name}\0")
|
||||
end
|
||||
|
||||
@buffer.write_header(data_offset: file_data_offset, name_offset: file_name_offset)
|
||||
ensure
|
||||
@buffer&.close
|
||||
end
|
||||
|
||||
def write_i32(int)
|
||||
@buffer.write([int].pack("l"))
|
||||
end
|
||||
|
||||
def write_u32(uint)
|
||||
@buffer.write([uint].pack("L"))
|
||||
end
|
||||
|
||||
def write_byte(byte)
|
||||
@buffer.write([byte].pack("c"))
|
||||
end
|
||||
end
|
||||
|
||||
# Eager loads patch file and streams target file metadata (doen't load target file data or generate CRCs)
|
||||
# after target file metadata is loaded, create a temp file and merge patched files into list then
|
||||
# build ordered file list and stream patched files and target file chunks into temp file,
|
||||
# after that is done, replace target file with temp file
|
||||
class Patcher
|
||||
def initialize(patch_files:, target_file:, temp_file:, buffer_size: DEFAULT_BUFFER_SIZE)
|
||||
@patch_files = patch_files.to_a.map { |f| Reader.new(file_path: f) }
|
||||
@target_file = File.open(target_file)
|
||||
@temp_file = File.open(temp_file, "a+b")
|
||||
@buffer_size = buffer_size
|
||||
end
|
||||
end
|
||||
|
||||
class Package
|
||||
attr_reader :files
|
||||
|
||||
def initialize(files: [])
|
||||
@files = files
|
||||
end
|
||||
|
||||
class File
|
||||
attr_accessor :name, :mix_crc, :content_offset, :content_length, :data
|
||||
|
||||
def initialize(name:, mix_crc: nil, content_offset: nil, content_length: nil, data: nil)
|
||||
@name = name
|
||||
@mix_crc = mix_crc
|
||||
@content_offset = content_offset
|
||||
@content_length = content_length
|
||||
@data = data
|
||||
end
|
||||
|
||||
def file_crc
|
||||
return "e6fe46b8" if @name.downcase == ".w3dhub.patch"
|
||||
|
||||
Digest::CRC32.hexdigest(@name.upcase)
|
||||
end
|
||||
|
||||
def data_crc
|
||||
Digest::CRC32.hexdigest(@data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -47,9 +47,5 @@ class W3DHub
|
||||
|
||||
def button_up(id)
|
||||
end
|
||||
|
||||
def menu(host_element, items:)
|
||||
@host.menu(host_element, items: items)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,6 +3,7 @@ class W3DHub
|
||||
class Community < Page
|
||||
def setup
|
||||
@w3dhub_news ||= nil
|
||||
@w3dhub_news_expires ||= 0
|
||||
|
||||
body.clear do
|
||||
stack(width: 1.0, height: 1.0, padding: 8) do
|
||||
@@ -76,6 +77,30 @@ class W3DHub
|
||||
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
|
||||
lock = Cache.acquire_net_lock("w3dhub_news")
|
||||
return false unless lock
|
||||
@@ -86,10 +111,11 @@ class W3DHub
|
||||
return unless news
|
||||
|
||||
news.items[0..15].each do |item|
|
||||
Cache.fetch(uri: item.image, async: false)
|
||||
Cache.fetch(uri: item.image, async: false, backend: :w3dhub)
|
||||
end
|
||||
|
||||
@w3dhub_news = news
|
||||
@w3dhub_news_expires = Gosu.milliseconds + (60 * 60 * 1000) # 1 hour (in ms)
|
||||
|
||||
"w3dhub_news"
|
||||
end
|
||||
@@ -135,7 +161,13 @@ class W3DHub
|
||||
flow(width: 1.0, max_width: 1230, height: 200, margin: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(0xff_252525))) do
|
||||
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, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(0xff_252525))) do
|
||||
tagline "<b>#{item.title}</b>", width: 1.0
|
||||
|
||||
@@ -36,7 +36,7 @@ class W3DHub
|
||||
background app_color
|
||||
|
||||
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
|
||||
|
||||
stack(margin_left: 8, width: 0.75) do
|
||||
|
||||
@@ -17,8 +17,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
# 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
|
||||
# , background_image: "C:/Users/cyber/Downloads/vlcsnap-2022-04-24-22h24m15s854.png"
|
||||
@game_page_container = stack(width: 1.0, fill: true) do
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,6 +28,36 @@ class W3DHub
|
||||
populate_games_list
|
||||
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
|
||||
@games_list_container.clear do
|
||||
background 0xaa_121920
|
||||
@@ -56,11 +85,11 @@ class W3DHub
|
||||
padding_left: 4, padding_right: 4, tip: game.name) do
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# para game.name, width: 1.0, text_align: :center
|
||||
@@ -71,7 +100,9 @@ class W3DHub
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -87,32 +118,30 @@ class W3DHub
|
||||
|
||||
@game_page_container.clear do
|
||||
game_color = Gosu::Color.new(game.color)
|
||||
game_color.alpha = 0x88
|
||||
game_color.alpha = 0xaa
|
||||
|
||||
background game_color
|
||||
@game_page_container.style.background_image_color = game_color
|
||||
@game_page_container.style.default[:background_image_color] = game_color
|
||||
@game_page_container.update_background_image
|
||||
background_image_path = Cache.package_path(game.category, game.id, "background.png", "")
|
||||
if File.exist?(background_image_path)
|
||||
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path)
|
||||
end
|
||||
|
||||
# Game Stuff
|
||||
flow(width: 1.0, fill: true) do
|
||||
# background 0xff_9999ff
|
||||
|
||||
# Game options
|
||||
stack(width: 360, height: 1.0, padding: 8, scroll: true, border_thickness_right: 1, border_color_right: W3DHub::BORDER_COLOR) do
|
||||
background 0x55_000000
|
||||
stack(width: 360, height: 1.0, padding: 8, scroll: true, background: game_color, border_thickness_right: 1, border_color_right: W3DHub::BORDER_COLOR) do
|
||||
# Game Logo
|
||||
logo_image_path = Cache.package_path(game.category, game.id, "logo.png", "")
|
||||
|
||||
# Game Banner
|
||||
image_path = "#{GAME_ROOT_PATH}/media/banners/#{game.id}.png"
|
||||
|
||||
if File.exist?(image_path)
|
||||
image image_path, width: 1.0
|
||||
if File.exist?(logo_image_path)
|
||||
image logo_image_path, width: 1.0
|
||||
else
|
||||
banner game.name unless File.exist?(image_path)
|
||||
banner game.name unless File.exist?(logo_image_path)
|
||||
end
|
||||
|
||||
stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do
|
||||
if Store.application_manager.installed?(game.id, channel.id)
|
||||
para "v#{Store.application_manager.installed?(game.id, channel.id)[:installed_version]}"
|
||||
|
||||
Hash.new.tap { |hash|
|
||||
# hash[I18n.t(:"games.game_settings")] = { icon: "gear", block: proc { Store.application_manager.settings(game.id, channel.id) } }
|
||||
# hash[I18n.t(:"games.wine_configuration")] = { icon: "gear", block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix?
|
||||
@@ -185,20 +214,38 @@ class W3DHub
|
||||
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|
|
||||
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) } }
|
||||
# items << { label: I18n.t(:"games.game_settings"), block: proc { Store.application_manager.settings(game.id, channel.id) } }
|
||||
items << { label: I18n.t(:"games.wine_configuration"), block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix?
|
||||
items << { label: I18n.t(:"games.game_modifications"), block: proc { populate_game_modifications(game, channel) } } unless Store.offline_mode
|
||||
if game.id != "ren"
|
||||
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
|
||||
if W3DHub.unix?
|
||||
menu_item(I18n.t(:"games.wine_configuration")) do
|
||||
Store.application_manager.wine_configuration(game.id, channel.id)
|
||||
end
|
||||
end
|
||||
|
||||
# From gui_state_ext.rb
|
||||
# TODO: Implement in engine proper
|
||||
menu(btn, items: items)
|
||||
unless Store.offline_mode
|
||||
if W3DHUB_DEVELOPER
|
||||
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
|
||||
|
||||
else
|
||||
@@ -230,7 +277,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
# Game Events
|
||||
@game_events_container = flow(width: 1.0, height: 128, padding: 8, visible: false) do
|
||||
@game_events_container = stack(width: 1.0, height: 128, padding: 8, scroll: true, visible: false) do
|
||||
end
|
||||
|
||||
# Game News
|
||||
@@ -282,11 +329,6 @@ class W3DHub
|
||||
|
||||
def populate_all_games_view
|
||||
@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_channel = nil
|
||||
|
||||
@@ -323,10 +365,10 @@ class W3DHub
|
||||
flow(width: 1.0, fill: true, scroll: true) do
|
||||
Store.applications.games.each do |game|
|
||||
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
|
||||
toggle_button checked: Store.application_manager.favorite?(game.id), text_size: 22, padding_top: 3, padding_right: 3, padding_bottom: 3, padding_left: 3 do |btn|
|
||||
Store.application_manager.favorive(game.id, btn.value)
|
||||
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.favorite(game.id, btn.value)
|
||||
Store.settings.save_settings
|
||||
|
||||
populate_games_list
|
||||
@@ -334,7 +376,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
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(fill: true)
|
||||
image image_path, width: 0.5
|
||||
@@ -376,10 +418,11 @@ class W3DHub
|
||||
return false unless news
|
||||
|
||||
news.items[0..15].each do |item|
|
||||
Cache.fetch(uri: item.image, async: false)
|
||||
Cache.fetch(uri: item.image, async: false, backend: :w3dhub)
|
||||
end
|
||||
|
||||
@game_news[game.id] = news
|
||||
@game_news["#{game.id}_expires"] = Gosu.milliseconds + (60 * 60 * 1000) # 1 hour (in ms)
|
||||
|
||||
"game_news_#{game.id}"
|
||||
end
|
||||
@@ -388,6 +431,9 @@ class W3DHub
|
||||
return unless @focused_game == game
|
||||
|
||||
if (feed = @game_news[game.id])
|
||||
game_color = Gosu::Color.new(game.color)
|
||||
game_color.alpha = 0xaa
|
||||
|
||||
@game_news_container.clear do
|
||||
# Patch Notes
|
||||
if false # Patch notes
|
||||
@@ -417,10 +463,10 @@ class W3DHub
|
||||
feed.items.sort_by { |i| i.timestamp }.reverse[0..9].each do |item|
|
||||
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
|
||||
background 0x44_000000
|
||||
|
||||
image image_path, height: 1.0
|
||||
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
|
||||
if File.file?(image_path)
|
||||
image image_path, height: 1.0
|
||||
end
|
||||
|
||||
stack(fill: true, height: 1.0, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(game.color))) do
|
||||
tagline "<b>#{item.title}</b>", width: 1.0
|
||||
@@ -466,15 +512,15 @@ class W3DHub
|
||||
@game_events_container.show unless events.empty?
|
||||
@game_events_container.hide if events.empty?
|
||||
|
||||
@game_events_container.clear do
|
||||
events.flatten.each do |event|
|
||||
stack(fill: true, height: 1.0, margin_left: 8, margin_right: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
|
||||
background 0x44_000000
|
||||
return unless (event = events.flatten.first)
|
||||
|
||||
title event.title, width: 1.0, text_align: :center
|
||||
title event.start_time.strftime("%A"), width: 1.0, text_align: :center
|
||||
caption event.start_time.strftime("%B %e, %Y %l:%M %p"), width: 1.0, text_align: :center
|
||||
end
|
||||
@game_events_container.clear do
|
||||
stack(width: 1.0, fill: true, margin_left: 8, margin_right: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
|
||||
background 0x44_000000
|
||||
|
||||
title event.title, width: 1.0, text_align: :center
|
||||
title event.start_time.strftime("%A"), width: 1.0, text_align: :center
|
||||
caption event.start_time.strftime("%B %e, %Y %l:%M %p"), width: 1.0, text_align: :center
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -521,4 +567,4 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,8 +44,10 @@ class W3DHub
|
||||
Store.settings[:account][:data] = account
|
||||
Store.settings.save_settings
|
||||
|
||||
Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false) if account
|
||||
applications = Api.applications if account
|
||||
if account
|
||||
Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false, backend: :w3dhub)
|
||||
applications = Api._applications
|
||||
end
|
||||
end
|
||||
|
||||
[account, applications]
|
||||
@@ -79,7 +81,7 @@ class W3DHub
|
||||
|
||||
if Store.account
|
||||
BackgroundWorker.foreground_job(
|
||||
-> { Cache.fetch(uri: Store.account.avatar_uri, async: false) },
|
||||
-> { Cache.fetch(uri: Store.account.avatar_uri, async: false, backend: :w3dhub) },
|
||||
->(result) {
|
||||
populate_account_info
|
||||
page(W3DHub::Pages::Games)
|
||||
@@ -102,9 +104,15 @@ class W3DHub
|
||||
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
|
||||
avatar_image = get_image(Cache.path(Store.account.avatar_uri))
|
||||
avatar_image = begin
|
||||
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")
|
||||
|
||||
composite_image = Gosu.render(256, 256) do
|
||||
@@ -146,7 +154,7 @@ class W3DHub
|
||||
Store.account = nil
|
||||
|
||||
BackgroundWorker.foreground_job(
|
||||
-> { Api.applications },
|
||||
-> { Api._applications },
|
||||
lambda do |applications|
|
||||
if applications
|
||||
Store.applications = applications
|
||||
|
||||
@@ -3,7 +3,7 @@ class W3DHub
|
||||
class ServerBrowser < Page
|
||||
def setup
|
||||
@server_locked_icons = {}
|
||||
@refresh_server_list = false
|
||||
@refresh_server_list_at_ms = nil
|
||||
refresh_server = false
|
||||
|
||||
@selected_server ||= nil
|
||||
@@ -43,7 +43,7 @@ class W3DHub
|
||||
app = Store.applications.games.find { |a| a.id == app_id.to_s }
|
||||
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,
|
||||
border_thickness_bottom: 1, border_color_bottom: 0x00_000000,
|
||||
@@ -63,19 +63,13 @@ class W3DHub
|
||||
populate_server_list
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
para "#{I18n.t(:"server_browser.nickname")}:"
|
||||
@@ -144,8 +138,8 @@ class W3DHub
|
||||
def update
|
||||
super
|
||||
|
||||
if @refresh_server_list && Gosu.milliseconds >= @refresh_server_list
|
||||
@refresh_server_list = nil
|
||||
if @refresh_server_list_at_ms && Gosu.milliseconds >= @refresh_server_list_at_ms
|
||||
@refresh_server_list_at_ms = nil
|
||||
|
||||
# populate_server_list
|
||||
reorder_server_list
|
||||
@@ -202,11 +196,11 @@ class W3DHub
|
||||
|
||||
def ping_icon(server)
|
||||
case server.ping
|
||||
when 0..160
|
||||
when 0..150
|
||||
@ping_icons[:good]
|
||||
when 161..250
|
||||
when 151..200
|
||||
@ping_icons[:fair]
|
||||
when 251..1_000
|
||||
when 201..1_000
|
||||
@ping_icons[:poor]
|
||||
when 1_001..5_000
|
||||
@ping_icons[:bad]
|
||||
@@ -216,27 +210,31 @@ class W3DHub
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def find_element_by_tag(container, tag, list = [])
|
||||
return unless container
|
||||
|
||||
container.children.each do |child|
|
||||
list << child if child.style.tag == tag
|
||||
|
||||
find_element_by_tag(child, tag, list) if child.is_a?(CyberarmEngine::Element::Container)
|
||||
def refresh_server_list(server, mode = :update) # :remove, :refresh_all
|
||||
if mode == :refresh_all
|
||||
populate_server_list
|
||||
return
|
||||
end
|
||||
|
||||
return list.first
|
||||
end
|
||||
|
||||
def refresh_server_list(server)
|
||||
@refresh_server_list = Gosu.milliseconds + 3_000
|
||||
@refresh_server_list_at_ms = Gosu.milliseconds + 3_000
|
||||
@refresh_server = server if @selected_server&.id == 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
|
||||
|
||||
game_icon = find_element_by_tag(server_container, :game_icon)
|
||||
@@ -247,13 +245,14 @@ class W3DHub
|
||||
player_count = find_element_by_tag(server_container, :player_count)
|
||||
server_ping = find_element_by_tag(server_container, :ping)
|
||||
|
||||
server_name.value = "<b>#{server&.status&.name}</b>"
|
||||
server_channel.value = Store.application_manager.channel_name(server.game, server.channel).to_s
|
||||
server_region.value = server.region
|
||||
server_map.value = server&.status&.map
|
||||
player_count.value = "#{server&.status&.player_count}/#{server&.status&.max_players}"
|
||||
server_ping.value = ping_icon(server)
|
||||
server_ping.parent.parent.tip = ping_tip(server)
|
||||
game_icon&.value = game_icon(server)
|
||||
server_name&.value = "<b>#{server&.status&.name}</b>"
|
||||
server_channel&.value = Store.application_manager.channel_name(server.game, server.channel).to_s
|
||||
server_region&.value = server.region
|
||||
server_map&.value = server&.status&.map
|
||||
player_count&.value = "#{server&.status&.player_count}/#{server&.status&.max_players}"
|
||||
server_ping&.value = ping_icon(server)
|
||||
server_ping&.parent.parent.tip = ping_tip(server)
|
||||
end
|
||||
|
||||
def update_server_ping(server)
|
||||
@@ -272,93 +271,94 @@ class W3DHub
|
||||
def stylize_selected_server(server_container)
|
||||
server_container.style.background = @selected_color
|
||||
|
||||
server_container.style.default[:background] = @selected_color
|
||||
server_container.style.hover[:background] = @selected_color
|
||||
server_container.style.active[:background] = @selected_color
|
||||
server_container.style.hover.background = @selected_color
|
||||
server_container.style.active.background = @selected_color
|
||||
end
|
||||
|
||||
def reorder_server_list
|
||||
@server_list_container.children.sort_by! do |child|
|
||||
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|
|
||||
next if @selected_server_container && child == @selected_server_container
|
||||
|
||||
child.style.hover[:background] = 0xaa_555566
|
||||
child.style.hover[:active] = 0xaa_555588
|
||||
child.style.hover.background = 0xaa_555566
|
||||
child.style.active.background = 0xaa_555588
|
||||
|
||||
child.style.default[:background] = 0xaa_333333 if i.even?
|
||||
child.style.default[:background] = 0x00_000000 if i.odd?
|
||||
child.style.background = 0xaa_333333 if i.even?
|
||||
child.style.background = 0x00_000000 if i.odd?
|
||||
end
|
||||
|
||||
@server_list_container.recalculate
|
||||
end
|
||||
|
||||
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
|
||||
i = -1
|
||||
|
||||
Store.server_list.each do |server|
|
||||
next unless @filters[server.game.to_sym]
|
||||
next unless server.region == @filter_region || @filter_region == "Any"
|
||||
next unless Store.application_manager.channel_name(server.game, server.channel) # can user access required game and channel for this server?
|
||||
|
||||
i += 1
|
||||
|
||||
server_container = flow(width: 1.0, height: 56, hover: { background: 0xaa_555566 }, active: { background: 0xaa_555588 }, tag: server.id, tip: ping_tip(server)) do
|
||||
background 0x88_333333 if i.even?
|
||||
|
||||
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
|
||||
create_server_container(server)
|
||||
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 server.version, margin_right: 8, tag: :server_version
|
||||
para Store.application_manager.channel_name(server.game, server.channel).to_s, width: 148, 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
|
||||
|
||||
def populate_server_info(server)
|
||||
@@ -369,7 +369,7 @@ class W3DHub
|
||||
flow(fill: true)
|
||||
|
||||
image game_icon(server), height: 1.0
|
||||
title server.status.name, text_wrap: :none
|
||||
title server.status.name[0..30], text_wrap: :none
|
||||
|
||||
flow(fill: true)
|
||||
end
|
||||
@@ -377,10 +377,12 @@ class W3DHub
|
||||
flow(width: 1.0, height: 46, margin_top: 16, margin_bottom: 16) do
|
||||
game_installed = Store.application_manager.installed?(server.game, server.channel)
|
||||
game_updatable = Store.application_manager.updateable?(server.game, server.channel)
|
||||
style = server.channel != "release" ? TESTING_BUTTON : {}
|
||||
matching_version = (game_installed && game_installed[:installed_version] == server.version) || server.version == Api::ServerListServer::NO_OR_DEFAULT_VERSION
|
||||
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)
|
||||
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 && matching_version), **style do
|
||||
# Check for nickname
|
||||
# prompt for nickname
|
||||
# !abort unless nickname set
|
||||
@@ -397,11 +399,11 @@ class W3DHub
|
||||
if server.status.password
|
||||
W3DHub.prompt_for_password(
|
||||
accept_callback: proc do |password|
|
||||
W3DHub.join_server(server, password)
|
||||
W3DHub.join_server(server: server, password: password)
|
||||
end
|
||||
)
|
||||
else
|
||||
W3DHub.join_server(server, nil)
|
||||
W3DHub.join_server(server: server)
|
||||
end
|
||||
end
|
||||
)
|
||||
@@ -409,18 +411,24 @@ class W3DHub
|
||||
if server.status.password
|
||||
W3DHub.prompt_for_password(
|
||||
accept_callback: proc do |password|
|
||||
W3DHub.join_server(server, password)
|
||||
W3DHub.join_server(server: server, password: password)
|
||||
end
|
||||
)
|
||||
else
|
||||
W3DHub.join_server(server, nil)
|
||||
W3DHub.join_server(server: server)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if W3DHUB_DEVELOPER
|
||||
list_box(items: (1..12).to_a.map(&:to_s), margin_left: 16, width: 72, tip: "Number of game clients", **TESTING_BUTTON)
|
||||
button "Multijoin", tip: "Launch multiple clients with configured username_\#{number}", **TESTING_BUTTON, enabled: true
|
||||
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}", 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
|
||||
|
||||
flow(fill: true)
|
||||
@@ -521,7 +529,7 @@ class W3DHub
|
||||
end
|
||||
|
||||
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
|
||||
@server_locked_icons[server.game] ||= Gosu.render(96, 96) do
|
||||
|
||||
@@ -3,82 +3,92 @@ class W3DHub
|
||||
class Settings < Page
|
||||
def setup
|
||||
body.clear do
|
||||
stack(width: 1.0, height: 1.0, padding: 16, scroll: true) do
|
||||
stack(width: 1.0, height: 1.0, padding: 16) do
|
||||
background 0xaa_252525
|
||||
|
||||
para "<b>Language</b>"
|
||||
flow(width: 1.0, height: 0.12) do
|
||||
para "<b>Launcher Language</b>", width: 0.249, margin_left: 32, margin_top: 12
|
||||
stack(width: 0.75) do
|
||||
@language_menu = list_box items: I18n.available_locales.map { |l| expand_language_code(l.to_s) }, choose: expand_language_code(Store.settings[:language]), width: 1.0
|
||||
para "Select the UI language you'd like to use in the W3D Hub Launcher."
|
||||
stack(width: 1.0, fill: true, max_width: 720, h_align: :center, scroll: true) do
|
||||
tagline "Launcher Language"
|
||||
@language_menu = list_box items: I18n.available_locales.map { |l| expand_language_code(l.to_s) }, choose: expand_language_code(Store.settings[:language]), width: 1.0, margin_left: 16
|
||||
para "Select the UI language you'd like to use in the W3D Hub Launcher.", margin_left: 16
|
||||
|
||||
|
||||
tagline "Launcher Directories", margin_top: 16
|
||||
caption "Applications Install Directory", margin_left: 16
|
||||
flow(width: 1.0, margin_left: 16) do
|
||||
@app_install_dir_input = edit_line Store.settings[:app_install_dir], fill: true
|
||||
button "Browse...", width: 128, tip: "Browse for applications install directory" do
|
||||
path = W3DHub.ask_folder
|
||||
@app_install_dir_input.value = path unless path.empty?
|
||||
end
|
||||
end
|
||||
|
||||
caption "Package Cache Directory", margin_left: 16, margin_top: 16
|
||||
flow(width: 1.0, 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
|
||||
|
||||
if W3DHub.unix?
|
||||
tagline "Wine - Windows compatibility layer", margin_top: 16
|
||||
caption "Wine Command", margin_left: 16
|
||||
flow(width: 1.0, margin_left: 16) do
|
||||
@wine_command_input = edit_line Store.settings[:wine_command], fill: true
|
||||
button "Browse...", width: 128, tip: "Browse for wine executable" do
|
||||
path = W3DHub.ask_file(filters: %w[wine proton])
|
||||
@wine_command_input.value = path unless path.empty?
|
||||
end
|
||||
end
|
||||
para "Command to use to for Windows compatiblity layer.", margin_left: 16
|
||||
|
||||
caption "Wine Prefix", margin_left: 16, margin_top: 16
|
||||
flow(width: 1.0, margin_left: 16) do
|
||||
@wine_prefix_input = edit_line Store.settings[:wine_prefix], fill: true
|
||||
button "Browse...", width: 128, tip: "Browse for wine prefix directory" do
|
||||
path = W3DHub.ask_folder
|
||||
@wine_prefix_input.value = path unless path.empty?
|
||||
end
|
||||
end
|
||||
para "Leave empty to use default global prefix.", margin_left: 16
|
||||
|
||||
link "Wiki: Getting Started With Wine", tip: "https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine", margin_top: 16, margin_left: 16, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine")
|
||||
end
|
||||
|
||||
# TODO: support winetricks stuff
|
||||
# tagline "Winetricks", margin_top: 16
|
||||
# caption "Winetricks Command", margin_left: 16
|
||||
# flow(width: 1.0, margin_left: 16) do
|
||||
# @winetricks_command_input = edit_line Store.settings[:winetricks_command], fill: true, enabled: false
|
||||
# button "Browse...", width: 128, tip: "Browse for winetricks executable", enabled: false do
|
||||
# path = W3DHub.ask_file(filters: %w[winetricks protontricks])
|
||||
# @winetricks_command_input.value = path unless path.empty?
|
||||
# end
|
||||
# end
|
||||
|
||||
# caption "Fixups", margin_left: 16, margin_top: 16
|
||||
# button "Install d3dcompiler_47", margin_left: 16, enabled: false
|
||||
# para "Fixes games instantly crashing at startup due to not being able to compile shaders.", margin_left: 16
|
||||
|
||||
# button "Install DXVK", margin_left: 16, margin_top: 16, enabled: false
|
||||
# para "Use Vulkan-based DirectX translation layers.", margin_left: 16
|
||||
# para "WARNING: Games will stop working if your hardware does not support Vulkan!", margin_left: 16
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
@app_install_dir_input = edit_line Store.settings[:app_install_dir], width: 1.0
|
||||
para "The folder into which new games and apps will be installed by the launcher"
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, margin_top: 16) do
|
||||
para "<b>Package Cache Folder</b>", width: 0.249, margin_left: 32, margin_top: 12
|
||||
|
||||
stack(width: 0.75) do
|
||||
@package_cache_dir_input = edit_line Store.settings[:package_cache_dir], width: 1.0
|
||||
para "A folder which will be used to cache downloaded packages used to install games and apps"
|
||||
end
|
||||
flow(width: 256, height: 64, h_align: :center, margin_top: 16) do
|
||||
button "Save", width: 1.0 do
|
||||
save_settings!
|
||||
end
|
||||
flow(fill: true)
|
||||
end
|
||||
|
||||
if true # W3DHub.unix?
|
||||
para "<b>Wine</b>", margin_top: 8, padding_top: 8, border_thickness_top: 2, border_color_top: 0xee_ffffff, width: 1.0
|
||||
flow(width: 1.0, height: 0.12) do
|
||||
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
|
||||
para "Command to use to for Windows compatiblity layer"
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 0.13, margin_top: 16) do
|
||||
para "<b>Wine Prefix</b>", width: 0.249, margin_left: 32, margin_top: 12
|
||||
stack(width: 0.75) do
|
||||
@wine_prefix_toggle = toggle_button checked: Store.settings[:wine_prefix]
|
||||
para "Whether each game gets its own prefix. Uses global/default prefix by default."
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
button "Save" do
|
||||
old_language = Store.settings[:language]
|
||||
Store.settings[:language] = language_code(@language_menu.value)
|
||||
|
||||
Store.settings[:app_install_dir] = @app_install_dir_input.value
|
||||
Store.settings[:package_cache_dir] = @package_cache_dir_input.value
|
||||
|
||||
Store.settings[:wine_command] = @wine_command_input.value
|
||||
Store.settings[:wine_prefix] = @wine_prefix_toggle.value
|
||||
|
||||
Store.settings.save_settings
|
||||
|
||||
begin
|
||||
I18n.locale = Store.settings[:language]
|
||||
rescue I18n::InvalidLocale
|
||||
I18n.locale = :en
|
||||
end
|
||||
|
||||
if old_language == Store.settings[:language]
|
||||
page(Pages::Games)
|
||||
else
|
||||
# pop back to Boot state which will immediately push a new instance of Interface
|
||||
pop_state
|
||||
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)}", tip: "Purge #{Store.settings[:package_cache_dir]}", **DANGEROUS_BUTTON) do |btn|
|
||||
logger.info(LOG_TAG) { "Purging cache (#{Store.settings[:package_cache_dir]})..." }
|
||||
FileUtils.remove_dir(Store.settings[:package_cache_dir], force: true)
|
||||
btn.value = "Clear package cache: #{W3DHub.format_size(Dir.glob("#{Store.settings[:package_cache_dir]}/**/**").map { |f| File.file?(f) ? File.size(f) : 0}.sum)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -108,7 +118,37 @@ class W3DHub
|
||||
when "es"
|
||||
"Español"
|
||||
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_input.value
|
||||
|
||||
Store.settings[:winetricks_command] = @winetricks_command_input.value if @winetricks_command_input
|
||||
|
||||
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
|
||||
|
||||
@@ -2,12 +2,13 @@ class W3DHub
|
||||
class Settings
|
||||
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,
|
||||
package_cache_dir: default_package_cache_dir,
|
||||
parallel_downloads: 4,
|
||||
wine_command: "wine",
|
||||
create_wine_prefixes: true,
|
||||
wine_prefix: "",
|
||||
winetricks_command: "winetricks",
|
||||
allow_diagnostic_reports: false,
|
||||
server_list_username: "",
|
||||
server_list_filters: {},
|
||||
@@ -66,10 +67,28 @@ class W3DHub
|
||||
|
||||
def load_settings
|
||||
@settings = JSON.parse(File.read(SETTINGS_FILE_PATH), symbolize_names: true)
|
||||
|
||||
# FIXUPS
|
||||
# FOR: v0.9.0
|
||||
@settings.delete(:create_wine_prefixes)
|
||||
@settings[:wine_prefix] ||= ""
|
||||
@settings[:winetricks_command] ||= "winetricks"
|
||||
|
||||
@settings
|
||||
end
|
||||
|
||||
def save_settings
|
||||
File.write(SETTINGS_FILE_PATH, @settings.to_json)
|
||||
File.write(SETTINGS_FILE_PATH, JSON.pretty_generate(@settings))
|
||||
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
|
||||
|
||||
@@ -11,26 +11,29 @@ class W3DHub
|
||||
@fraction = 0.0
|
||||
@w3dhub_logo = get_image("#{GAME_ROOT_PATH}/media/icons/app.png")
|
||||
@tasks = {
|
||||
# connectivity_check: { started: false, complete: false }, # HEAD connectivity-check.ubuntu.com or HEAD secure.w3dhub.com?
|
||||
connectivity_check: { started: false, complete: false }, # HEAD connectivity-check.ubuntu.com or HEAD secure.w3dhub.com?
|
||||
# launcher_updater: { started: false, complete: false },
|
||||
server_list: { started: false, complete: false },
|
||||
refresh_user_token: { started: false, complete: false },
|
||||
service_status: { started: false, complete: false },
|
||||
applications: { 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
|
||||
|
||||
@task_index = 0
|
||||
|
||||
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, 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
|
||||
end
|
||||
|
||||
stack(width: 1.0, height: 60) do
|
||||
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)}...", width: 0.5
|
||||
para "#{I18n.t(:app_name)} #{W3DHub::VERSION}", width: 0.5, text_align: :right
|
||||
@status_label = caption "Starting #{I18n.t(:app_name_simple)}...", fill: true
|
||||
para "#{I18n.t(:app_name)} #{W3DHub::VERSION}", text_align: :right
|
||||
end
|
||||
|
||||
@progressbar = progress height: 4, width: 1.0, margin_left: 16, margin_right: 16, margin_bottom: 8
|
||||
@@ -39,7 +42,8 @@ class W3DHub
|
||||
end
|
||||
|
||||
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, 0xaa_353535, 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)
|
||||
|
||||
super
|
||||
@@ -52,10 +56,35 @@ class W3DHub
|
||||
|
||||
@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)
|
||||
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)
|
||||
end
|
||||
|
||||
@@ -102,7 +131,7 @@ class W3DHub
|
||||
|
||||
Store.settings[:account][:data] = account
|
||||
|
||||
Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false)
|
||||
Cache.fetch(uri: account.avatar_uri, force_fetch: true, async: false, backend: :w3dhub)
|
||||
else
|
||||
Store.settings[:account] = {}
|
||||
end
|
||||
@@ -112,6 +141,39 @@ class W3DHub
|
||||
@tasks[:refresh_user_token][:complete] = true
|
||||
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|
|
||||
Resolv.getaddress(key.to_s)
|
||||
rescue StandardError => e
|
||||
logger.error(LOG_TAG) { "Failed to resolve hostname: #{key}" }
|
||||
logger.error(LOG_TAG) { e }
|
||||
|
||||
push_state(
|
||||
ConfirmDialog,
|
||||
title: "DNS Resolution Failure",
|
||||
message: "Failed to resolve: #{key}\n\nTry disabling VPN or proxy if in use.\n\n\nContinue offline?",
|
||||
cancel_callback: -> { window.close },
|
||||
accept_callback: lambda {
|
||||
@offline_mode = true
|
||||
Store.offline_mode = true
|
||||
@tasks[:connectivity_check][:complete] = true
|
||||
}
|
||||
)
|
||||
|
||||
# Prevent task from being marked as completed
|
||||
return false
|
||||
end
|
||||
|
||||
@tasks[:connectivity_check][:complete] = true
|
||||
end
|
||||
|
||||
def service_status
|
||||
Api.on_thread(:service_status) do |service_status|
|
||||
@service_status = service_status
|
||||
@@ -125,7 +187,9 @@ class W3DHub
|
||||
|
||||
@tasks[:service_status][:complete] = true
|
||||
else
|
||||
BackgroundWorker.foreground_job(-> {}, ->(_) { @status_label.value = I18n.t(:"boot.w3dhub_service_is_down") })
|
||||
BackgroundWorker.foreground_job(-> {}, lambda { |_|
|
||||
@status_label.value = I18n.t(:"boot.w3dhub_service_is_down")
|
||||
})
|
||||
@tasks[:service_status][:complete] = true
|
||||
|
||||
@offline_mode = true
|
||||
@@ -134,17 +198,43 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
|
||||
def launcher_updater
|
||||
@status_label.value = "Checking for Launcher updates..." # I18n.t(:"boot.checking_for_updates")
|
||||
|
||||
Api.on_thread(:fetch, "https://api.github.com/repos/cyberarm/w3d_hub_linux_launcher/releases/latest") do |response|
|
||||
if response.status == 200
|
||||
hash = JSON.parse(response.body, symbolize_names: true)
|
||||
available_version = hash[:tag_name].downcase.sub("v", "")
|
||||
|
||||
pp Gem::Version.new(available_version) > Gem::Version.new(W3DHub::VERSION)
|
||||
pp [Gem::Version.new(available_version), Gem::Version.new(W3DHub::VERSION)]
|
||||
|
||||
push_state(
|
||||
LauncherUpdaterDialog,
|
||||
release_data: hash,
|
||||
available_version: available_version,
|
||||
cancel_callback: -> { @tasks[:launcher_updater][:complete] = true },
|
||||
accept_callback: -> { @tasks[:launcher_updater][:complete] = true }
|
||||
)
|
||||
else
|
||||
# Failed to retrieve release data from github
|
||||
log "Failed to retrieve release data from Github"
|
||||
@tasks[:launcher_updater][:complete] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def applications
|
||||
@status_label.value = I18n.t(:"boot.checking_for_updates")
|
||||
|
||||
Api.on_thread(:applications) do |applications|
|
||||
Api.on_thread(:_applications) do |applications|
|
||||
if applications
|
||||
Store.applications = applications
|
||||
|
||||
Store.settings.save_application_cache(JSON.pretty_generate(applications.data))
|
||||
@tasks[:applications][:complete] = true
|
||||
else
|
||||
# FIXME: Failed to retreive!
|
||||
BackgroundWorker.foreground_job(-> {}, ->(_){ @status_label.value = "FAILED TO RETREIVE APPS LIST" })
|
||||
BackgroundWorker.foreground_job(-> {}, ->(_) { @status_label.value = "FAILED TO RETREIVE APPS LIST" })
|
||||
|
||||
@offline_mode = true
|
||||
Store.offline_mode = true
|
||||
@@ -155,35 +245,80 @@ class W3DHub
|
||||
def app_icons
|
||||
return unless Store.applications
|
||||
|
||||
@status_label.value = "Retrieving application icons, this might take a moment..." # I18n.t(:"boot.checking_for_updates")
|
||||
|
||||
packages = []
|
||||
failure = false
|
||||
Store.applications.games.each do |app|
|
||||
packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" }
|
||||
end
|
||||
|
||||
Api.on_thread(:package_details, packages) do |package_details|
|
||||
Api.on_thread(:package_details, packages, :alt_w3dhub) do |package_details|
|
||||
package_details ||= nil
|
||||
|
||||
package_details&.each do |package|
|
||||
next if package.error?
|
||||
|
||||
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
|
||||
|
||||
broken_or_out_dated_icon = Digest::SHA256.new.hexdigest(File.binread(path)).upcase != package.checksum.upcase if File.exist?(path)
|
||||
if File.exist?(path)
|
||||
broken_or_out_dated_icon = Digest::SHA256.new.hexdigest(File.binread(path)).upcase != package.checksum.upcase
|
||||
end
|
||||
|
||||
if File.exist?(path) && !broken_or_out_dated_icon
|
||||
regenerate = !File.exist?(generated_icon_path)
|
||||
else
|
||||
Cache.fetch_package(package, proc {})
|
||||
regenerate = true
|
||||
begin
|
||||
Cache.fetch_package(package, proc {})
|
||||
regenerate = true
|
||||
rescue Errno::EACCES => e
|
||||
failure = true
|
||||
push_state(MessageDialog, title: "Fatal Error",
|
||||
message: "Directory Permission Error (#{e.class}):\n#{e}.\n\nIs the required drive mounted?",
|
||||
accept_callback: -> { window.close })
|
||||
end
|
||||
end
|
||||
|
||||
if regenerate
|
||||
BackgroundWorker.foreground_job(-> { ICO.new(file: path) }, ->(result) { result.save(result.images.max_by(&:width), generated_icon_path) })
|
||||
end
|
||||
next unless regenerate
|
||||
|
||||
BackgroundWorker.foreground_job(-> { ICO.new(file: path) }, lambda { |result|
|
||||
result.save(result.images.max_by(&:width), generated_icon_path)
|
||||
})
|
||||
end
|
||||
|
||||
@tasks[:app_icons][:complete] = true
|
||||
@tasks[:app_icons][:complete] = true unless failure
|
||||
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
|
||||
|
||||
@@ -211,6 +346,12 @@ class W3DHub
|
||||
end
|
||||
|
||||
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 = {
|
||||
applications: []
|
||||
}
|
||||
@@ -228,7 +369,10 @@ class W3DHub
|
||||
"studio-id": "",
|
||||
channels: [],
|
||||
"web-links": [],
|
||||
"extended-data": [{ name: "colour", value: "#353535" }]
|
||||
"extended-data": [
|
||||
{ name: "colour", value: game[:colour] },
|
||||
{ name: "usesEngineCfg", value: game[:uses_engine_cfg] }
|
||||
]
|
||||
}
|
||||
|
||||
channel = {
|
||||
@@ -243,7 +387,7 @@ class W3DHub
|
||||
hash[:applications] << app unless app_in_array
|
||||
end
|
||||
|
||||
Store.applications = Api::Applications.new(hash.to_json)
|
||||
Store.applications = Api::Applications.new(hash.to_json) unless hash[:applications].empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ class W3DHub
|
||||
|
||||
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
|
||||
@options[:accept_callback]&.call
|
||||
end
|
||||
|
||||
@@ -2,6 +2,10 @@ class W3DHub
|
||||
class States
|
||||
class GameSettingsDialog < 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
|
||||
@@ -88,7 +92,7 @@ class W3DHub
|
||||
current_res = "#{@game_settings.get_value(:resolution_width)}x#{@game_settings.get_value(:resolution_height)}"
|
||||
|
||||
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)
|
||||
|
||||
@game_settings.set_value(:resolution_width, w.to_i)
|
||||
@@ -98,7 +102,7 @@ class W3DHub
|
||||
|
||||
flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -112,7 +116,7 @@ class W3DHub
|
||||
|
||||
flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -191,35 +195,35 @@ class W3DHub
|
||||
|
||||
flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
# flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
# 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)
|
||||
# end
|
||||
# end
|
||||
|
||||
# flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
# 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)
|
||||
# end
|
||||
# end
|
||||
|
||||
flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
@@ -233,7 +237,7 @@ class W3DHub
|
||||
|
||||
flow(width: 1.0, height: 28, margin: 4, margin_left: 10) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
@@ -24,6 +24,7 @@ class W3DHub
|
||||
stack(width: 1.0, height: 46, padding: 8) do
|
||||
button "Okay", width: 1.0 do
|
||||
pop_state
|
||||
@options[:accept_callback]&.call
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
class W3DHub
|
||||
class States
|
||||
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
|
||||
|
||||
@@instance = nil
|
||||
@@ -18,6 +23,10 @@ class W3DHub
|
||||
@service_status = @options[:service_status]
|
||||
@applications = @options[:applications]
|
||||
|
||||
@account_expire = Gosu.milliseconds
|
||||
@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
|
||||
|
||||
@page = nil
|
||||
@@ -27,10 +36,12 @@ class W3DHub
|
||||
|
||||
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
|
||||
|
||||
@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(fill: true)
|
||||
image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 84
|
||||
@@ -48,18 +59,22 @@ class W3DHub
|
||||
end
|
||||
|
||||
link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||
page(W3DHub::Pages::ServerBrowser)
|
||||
end
|
||||
|
||||
link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||
page(W3DHub::Pages::Community)
|
||||
end
|
||||
|
||||
link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||
page(W3DHub::Pages::DownloadManager)
|
||||
end
|
||||
|
||||
link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||
page(W3DHub::Pages::Settings)
|
||||
end
|
||||
end
|
||||
@@ -70,8 +85,8 @@ class W3DHub
|
||||
@application_taskbar_container = flow(width: 1.0, height: 0.5) do
|
||||
stack(width: 1.0, height: 1.0, margin_left: 16, margin_right: 16) do
|
||||
flow(width: 1.0, height: 0.65) do
|
||||
@application_taskbar_label = para "", width: 0.60, text_wrap: :none
|
||||
@application_taskbar_status_label = para "", width: 0.40, text_align: :right, text_wrap: :none
|
||||
@application_taskbar_label = para "", fill: true, text_wrap: :none
|
||||
@application_taskbar_status_label = para "", width: 0.4, min_width: 256, text_align: :right, text_wrap: :none
|
||||
end
|
||||
|
||||
@application_taskbar_progressbar = progress fraction: 0.0, height: 2, width: 1.0
|
||||
@@ -114,6 +129,13 @@ class W3DHub
|
||||
end
|
||||
|
||||
hide_application_taskbar
|
||||
|
||||
every(3_000) do
|
||||
# NOTE: each method called, internally checks whether it should act.
|
||||
refresh_account_token
|
||||
refresh_applications
|
||||
refresh_server_list
|
||||
end
|
||||
end
|
||||
|
||||
def draw
|
||||
@@ -163,10 +185,10 @@ class W3DHub
|
||||
@page
|
||||
end
|
||||
|
||||
def update_server_browser(server)
|
||||
def update_server_browser(server, mode = :update)
|
||||
return unless @page.is_a?(Pages::ServerBrowser)
|
||||
|
||||
@page.refresh_server_list(server)
|
||||
@page.refresh_server_list(server, mode)
|
||||
end
|
||||
|
||||
def update_server_ping(server)
|
||||
@@ -226,6 +248,63 @@ class W3DHub
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_account_token
|
||||
return if Gosu.milliseconds < @account_expire
|
||||
return unless account = Store.account
|
||||
|
||||
@account_expire = Gosu.milliseconds + 30_000
|
||||
|
||||
if (account.access_token_expiry - Time.now) / 60 <= 60 * 3 # Refresh if token expires within 3 hours
|
||||
logger.info(LOG_TAG) { "Refreshing user login..." }
|
||||
|
||||
Api.on_thread(:refresh_user_login, account.refresh_token) do |refreshed_account|
|
||||
if refreshed_account
|
||||
Store.account = refreshed_account
|
||||
|
||||
Store.settings[:account][:data] = refreshed_account
|
||||
else
|
||||
Store.settings[:account] = {}
|
||||
end
|
||||
|
||||
Store.settings.save_settings
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_applications
|
||||
return if Gosu.milliseconds < @applications_expire
|
||||
|
||||
@applications_expire = Gosu.milliseconds + 30_000
|
||||
|
||||
Api.on_thread(:_applications) do |applications|
|
||||
if applications
|
||||
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
|
||||
|
||||
Store.applications = applications
|
||||
|
||||
# TODO: Signal Games and ServerBrowser that applications have been updated
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_server_list
|
||||
return if Gosu.milliseconds < @server_list_expire
|
||||
|
||||
@server_list_expire = Gosu.milliseconds + 30_000
|
||||
|
||||
Api.on_thread(:server_list, 2) do |list|
|
||||
if list
|
||||
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # five minutes
|
||||
|
||||
Store.server_list_last_fetch = Gosu.milliseconds
|
||||
|
||||
Api::ServerListUpdater.instance.refresh_server_list(list)
|
||||
|
||||
BackgroundWorker.foreground_job(-> {}, ->(_) { States::Interface.instance&.update_server_browser(nil, :refresh_all) })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ class W3DHub
|
||||
flow(width: 1.0, height: 1.0, background_image: "#{GAME_ROOT_PATH}/media/banners/background.png", background_image_color: 0xff_525252, background_image_mode: :fill) do
|
||||
flow(fill: true)
|
||||
|
||||
@card_container = stack(width: 1.0, max_width: MAX_PAGE_WIDTH, height: 1.0, max_height: 720, margin: 128, padding: 16) do
|
||||
@card_container = stack(width: 1.0, max_width: MAX_PAGE_WIDTH, height: 1.0, max_height: 720, margin: 64, v_align: :center, h_align: :center, padding: 16) do
|
||||
background 0xaa_353535
|
||||
end
|
||||
|
||||
@@ -24,9 +24,12 @@ class W3DHub
|
||||
|
||||
def card_welcome
|
||||
stack(width: 1.0, fill: true) do
|
||||
banner "Welcome", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_000000
|
||||
banner "Welcome", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
|
||||
title "Welcome to the #{I18n.t(:app_name_simple)}"
|
||||
caption "The #{I18n.t(:app_name_simple)} is a one-stop shop for your W3D gaming needs, providing game downloads, automatic updating, an integrated server browser, and centralized management of in-game options.", width: 1.0, margin_left: 32
|
||||
caption "The #{I18n.t(:app_name_simple)} is a one-stop shop for your W3D gaming needs, providing game downloads, "\
|
||||
"automatic updating, an integrated server browser, and centralized management of in-game options.", width: 1.0, margin_left: 32
|
||||
|
||||
image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 256
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 46) do
|
||||
@@ -44,14 +47,25 @@ class W3DHub
|
||||
|
||||
def card_getting_started
|
||||
stack(width: 1.0, fill: true) do
|
||||
banner "Getting Started", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_000000
|
||||
title "Import C&C Renegade"
|
||||
caption "You can import your installed copy of Renegade if it wasn't automatically imported from the Games tab. If you need to procure a copy of Renegade, EA's Origin Store has the Command & Conquer The Ultimate Collection available. We cannot provide Renegade for installation.", width: 1.0, margin_left: 32
|
||||
banner "Getting Started", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
|
||||
title "Import Command & Conquer: Renegade"
|
||||
caption "You can import your installed copy of Renegade if it wasn't automatically imported from the Games tab.\n"\
|
||||
"If you need to procure a copy of Renegade, Both Steam and the EA App have the Command & Conquer The Ultimate Collection available for purchase. "\
|
||||
"We cannot provide Renegade for installation.", width: 1.0, margin_left: 32
|
||||
|
||||
stack(width: 1.0, height: 2, background: 0x88_ffffff)
|
||||
stack(width: 1.0, height: 2, background: 0xff_0074e0, margin_top: 16, margin_bottom: 16)
|
||||
|
||||
title "Install one of our standalone games"
|
||||
caption "Browse our selection of games from the left panel of the Games tab.\n• Interim Apex - Renegade but with hundreds of vehicles and characters.\n• Red Alert: A Path Beyond - DESCRIPTION\n• Tiberian Sun: Reborn - DESCRIPTION\n\nAnd more... Check out the left panel on the Games tab.", width: 1.0, margin_left: 32
|
||||
stack(width: 1.0, fill: true, margin_left: 32) do
|
||||
tagline "Interim Apex"
|
||||
caption "An expanded boots on the ground conflict set after the advent of Tiberian Dawn and the inter-war period between Tiberian Dawn and Tiberian Sun.", margin_left: 16
|
||||
tagline "Red Alert 2: Apocalypse Rising"
|
||||
caption "A multiplayer first-and-third-person shooter set in the vibrant universe of Command & Conquer: Red Alert 2. ", margin_left: 16
|
||||
tagline "Tiberian Sun: Reborn"
|
||||
caption "A standalone first-person shooter set in the Tiberian Sun universe.", margin_left: 16
|
||||
para ""
|
||||
caption "And more games! See them all on the Games tab."
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 46) do
|
||||
@@ -66,25 +80,22 @@ class W3DHub
|
||||
end
|
||||
|
||||
button "Next >" do
|
||||
@card_container.clear { card_communitiy }
|
||||
@card_container.clear { W3DHub.unix? ? card_wine : card_community }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def card_communitiy
|
||||
def card_wine
|
||||
stack(width: 1.0, fill: true) do
|
||||
banner "W3D Hub Community", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_000000
|
||||
title "Forums"
|
||||
caption "Join our forum community", margin_left: 32
|
||||
|
||||
title "Facebook"
|
||||
caption "Like us on Facebook", margin_left: 32
|
||||
|
||||
title "Discord"
|
||||
caption "Join our Discord community server", margin_left: 32
|
||||
|
||||
title "YouTube"
|
||||
caption "Subscribe to our YouTube channel", margin_left: 32
|
||||
banner "Wine - Windows compatibility layer", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
|
||||
stack(width: 1.0, fill: true, margin_left: 32) do
|
||||
title "Got Wine?"
|
||||
caption "The launcher requires a windows compatibility tool like wine in order to run the games.", margin_left: 32
|
||||
caption "Install wine and winetricks through your distribution's package manager or use a wine manager like Bottles.", margin_left: 32
|
||||
link "See most up to date instructions on the wiki.", tip: "https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine", margin_top: 16, margin_left: 32, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://github.com/cyberarm/w3d_hub_linux_launcher/wiki/Getting-Started-With-Wine")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 46) do
|
||||
@@ -92,6 +103,52 @@ class W3DHub
|
||||
button "< Back" do
|
||||
@card_container.clear { card_getting_started }
|
||||
end
|
||||
|
||||
link "Skip", border_color_bottom: 0xff_777777, margin_left: 16 do
|
||||
pop_state
|
||||
end
|
||||
end
|
||||
|
||||
button "Next >" do
|
||||
@card_container.clear { card_community }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def card_community
|
||||
stack(width: 1.0, fill: true) do
|
||||
banner "W3D Hub Community", width: 1.0, border_thickness_bottom: 4, border_color_bottom: 0xff_0074e0
|
||||
title "W3D Hub"
|
||||
link "Visit website", tip: "https://w3dhub.com", margin_left: 32, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://w3dhub.com")
|
||||
end
|
||||
|
||||
title "Forum"
|
||||
link "Join our forum community", tip: "https://w3dhub.com/forum", margin_left: 32, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://w3dhub.com/forum")
|
||||
end
|
||||
|
||||
title "Facebook"
|
||||
link "Like us on Facebook", tip: "https://www.facebook.com/w3dhub/", margin_left: 32, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://www.facebook.com/w3dhub/")
|
||||
end
|
||||
|
||||
title "Discord"
|
||||
link "Join our Discord community server", tip: "https://discord.gg/jMmmRa2", margin_left: 32, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://discord.gg/jMmmRa2")
|
||||
end
|
||||
|
||||
title "YouTube"
|
||||
link "Subscribe to our YouTube channel", tip: "https://www.youtube.com/@w3dhub-official", margin_left: 32, border_color_bottom: 0xff_777777 do
|
||||
W3DHub.url("https://www.youtube.com/@w3dhub-official")
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 46) do
|
||||
flow(fill: true, height: 1.0) do
|
||||
button "< Back" do
|
||||
@card_container.clear { W3DHub.unix? ? card_wine : card_getting_started }
|
||||
end
|
||||
end
|
||||
|
||||
button "Done" do
|
||||
|
||||
49
lib/theme.rb
@@ -7,12 +7,28 @@ class W3DHub
|
||||
MAX_PAGE_WIDTH = 1200
|
||||
|
||||
TESTING_BUTTON = {
|
||||
background: 0xff_ff8800,
|
||||
background: 0xff_ff8800..0xff_dd6600,
|
||||
border_color: Gosu::Color::NONE,
|
||||
hover: {
|
||||
background: 0xff_ffaa00
|
||||
background: 0xff_dd6600..0xff_bb4400,
|
||||
border_color: 0xff_ff8800,
|
||||
},
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,15 +96,15 @@ class W3DHub
|
||||
padding_bottom: 8,
|
||||
border_thickness: 2,
|
||||
border_color: Gosu::Color::NONE,
|
||||
background: 0xff_0074e0,
|
||||
background: 0xff_0074e0..0xff_0052c0,
|
||||
hover: {
|
||||
color: 0xff_f2f2f2,
|
||||
background: 0xff_004c94,
|
||||
background: 0xff_0052c0..0xff_0030a0,
|
||||
border_color: 0xff_0074e0
|
||||
},
|
||||
active: {
|
||||
color: 0xff_aaaaaa,
|
||||
background: 0xff_005aad,
|
||||
background: 0xff_0030a0..0xff_0052c0,
|
||||
border_color: 0xff_0074e0
|
||||
}
|
||||
},
|
||||
@@ -142,6 +158,27 @@ class W3DHub
|
||||
active: {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class W3DHub
|
||||
DIR_NAME = "W3DHubAlt"
|
||||
VERSION = "0.3.1"
|
||||
end
|
||||
DIR_NAME = "W3DHubAlt".freeze
|
||||
VERSION = "0.9.2".freeze
|
||||
end
|
||||
|
||||
75
lib/websocket_client.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
class W3DHub
|
||||
class WebSocketClient
|
||||
def initialize
|
||||
@errored = nil
|
||||
@connection = nil
|
||||
|
||||
@events = {
|
||||
open: nil,
|
||||
message: nil,
|
||||
close: nil,
|
||||
error: nil
|
||||
}
|
||||
end
|
||||
|
||||
def connect(endpoint, headers: nil, &block)
|
||||
yield(self)
|
||||
|
||||
Sync do |task|
|
||||
ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil
|
||||
ssl_context&.alpn_protocols = Async::HTTP::Protocol::HTTP11.names
|
||||
ssl_context&.set_params(
|
||||
ca_file: W3DHub.ca_bundle_path,
|
||||
verify_mode: OpenSSL::SSL::VERIFY_PEER
|
||||
)
|
||||
|
||||
endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names, ssl_context: ssl_context)
|
||||
|
||||
Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection|
|
||||
@connection = connection
|
||||
|
||||
@events[:open]&.call
|
||||
|
||||
while message = connection.read
|
||||
@events[:message].call(message)
|
||||
end
|
||||
# FIXME: Don't rescue for all ta errors?
|
||||
rescue => error
|
||||
@errored = true
|
||||
@events[:error]&.call(error)
|
||||
ensure
|
||||
@events[:close]&.call unless @errored
|
||||
@connection = nil
|
||||
@errored = false
|
||||
end
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def on(event, &block)
|
||||
raise "Event must be a symbol" unless event.is_a?(Symbol)
|
||||
raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event)
|
||||
raise "No block given for #{event.inspect}" unless block_given?
|
||||
|
||||
@events[event] = block
|
||||
end
|
||||
|
||||
def send(data, type: :text)
|
||||
@connection&.write(data)
|
||||
@connection&.flush
|
||||
end
|
||||
|
||||
def close
|
||||
@connection&.close
|
||||
end
|
||||
|
||||
def open?
|
||||
!closed?
|
||||
end
|
||||
|
||||
def closed?
|
||||
@connection&.closed?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,8 @@
|
||||
class W3DHub
|
||||
class Window < CyberarmEngine::Window
|
||||
def setup
|
||||
self.caption = I18n.t(:app_name)
|
||||
self.show_stats_plotter = false
|
||||
self.caption = "#{I18n.t(:app_name)} v#{VERSION}"
|
||||
|
||||
Store[:server_list] = []
|
||||
Store[:settings] = Settings.new
|
||||
@@ -9,11 +10,6 @@ class W3DHub
|
||||
|
||||
Store[:main_thread_queue] = []
|
||||
|
||||
# Repair/Upgrade schema
|
||||
Store.settings[:favorites] ||= {}
|
||||
|
||||
Store.settings.save_settings
|
||||
|
||||
begin
|
||||
I18n.locale = Store.settings[:language]
|
||||
rescue I18n::InvalidLocale
|
||||
@@ -21,8 +17,8 @@ class W3DHub
|
||||
end
|
||||
|
||||
# push_state(W3DHub::States::DemoInputDelay)
|
||||
# push_state(W3DHub::States::Welcome)
|
||||
push_state(W3DHub::States::Boot)
|
||||
push_state(W3DHub::States::Welcome) unless File.exist?(SETTINGS_FILE_PATH)
|
||||
# push_state(W3DHub::States::DirectConnectDialog)
|
||||
# push_state(W3DHub::Asterisk::States::IRCProfileForm)
|
||||
end
|
||||
|
||||
287
lib/ww_mix.rb
Normal file
@@ -0,0 +1,287 @@
|
||||
require "digest"
|
||||
require "stringio"
|
||||
|
||||
class W3DHub
|
||||
# Reimplementating MIX1 reader/writer with years more
|
||||
# experience working with these formats and having then
|
||||
# advantage of being able to reference the renegade source
|
||||
# code :)
|
||||
class WWMix
|
||||
MIX1_HEADER = 0x3158494D
|
||||
MIX2_HEADER = 0x3258494D
|
||||
|
||||
MixHeader = Struct.new(
|
||||
:mime_type, # int32
|
||||
:file_data_offset, # int32
|
||||
:file_names_offset, # int32
|
||||
:_reserved # int32
|
||||
)
|
||||
|
||||
EntryInfoHeader = Struct.new(
|
||||
:crc32, # uint32
|
||||
:content_offset, # uint32
|
||||
:content_length # uint32
|
||||
)
|
||||
|
||||
class Entry
|
||||
attr_accessor :path, :name, :info, :blob, :is_blob
|
||||
|
||||
def initialize(name:, path:, info:, blob: nil)
|
||||
@name = name
|
||||
@path = path
|
||||
@info = info
|
||||
@blob = blob
|
||||
|
||||
@info.content_length = blob.size if blob?
|
||||
end
|
||||
|
||||
def blob?
|
||||
@blob
|
||||
end
|
||||
|
||||
def calculate_crc32
|
||||
Digest::CRC32.hexdigest(@name.upcase).upcase.to_i(16)
|
||||
end
|
||||
|
||||
# Write entry's data to stream.
|
||||
# Caller is responsible for ensuring stream is valid for writing
|
||||
def copy_to(stream)
|
||||
if blob?
|
||||
return false if @blob.size.zero?
|
||||
|
||||
stream.write(blob)
|
||||
return true
|
||||
else
|
||||
if read
|
||||
stream.write(@blob)
|
||||
@blob = nil
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def read
|
||||
return false unless File.exist?(@path)
|
||||
return false if File.directory?(@path)
|
||||
return false if File.size(@path) < @info.content_offset + @info.content_length
|
||||
|
||||
@blob = File.binread(@path, @info.content_length, @info.content_offset)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :path, :encrypted, :entries, :error_reason
|
||||
|
||||
def initialize(path:, encrypted: false)
|
||||
@path = path
|
||||
@encrypted = encrypted
|
||||
@entries = []
|
||||
|
||||
@error_reason = ""
|
||||
end
|
||||
|
||||
# Load entries from MIX file. Entry data is NOT loaded.
|
||||
# @return true on success or false on failure. Check m_error_reason for why.
|
||||
def load
|
||||
unless File.exist?(@path)
|
||||
@error_reason = format("Path does not exist: %s", @path)
|
||||
return false
|
||||
end
|
||||
|
||||
if File.directory?(@path)
|
||||
@error_reason = format("Path is a directory: %s", @path)
|
||||
return false
|
||||
end
|
||||
|
||||
File.open(@path, "rb") do |f|
|
||||
header = MixHeader.new(0, 0, 0, 0)
|
||||
header.mime_type = read_i32(f)
|
||||
header.file_data_offset = read_i32(f)
|
||||
header.file_names_offset = read_i32(f)
|
||||
header._reserved = read_i32(f)
|
||||
|
||||
unless header.mime_type == MIX1_HEADER || header.mime_type == MIX2_HEADER
|
||||
@error_reason = format("Invalid mime type: %d", header.mime_type)
|
||||
return false
|
||||
end
|
||||
|
||||
@encrypted = header.mime_type == MIX2_HEADER
|
||||
|
||||
# Read entry info
|
||||
f.pos = header.file_data_offset
|
||||
file_count = read_i32(f)
|
||||
|
||||
file_count.times do |i|
|
||||
entry_info = EntryInfoHeader.new(0, 0, 0)
|
||||
entry_info.crc32 = read_u32(f)
|
||||
entry_info.content_offset = read_u32(f)
|
||||
entry_info.content_length = read_u32(f)
|
||||
|
||||
@entries << Entry.new(name: "", path: @path, info: entry_info)
|
||||
end
|
||||
|
||||
# Read entry names
|
||||
f.pos = header.file_names_offset
|
||||
file_count = read_i32(f)
|
||||
|
||||
file_count.times do |i|
|
||||
@entries[i].name = read_string(f)
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def save
|
||||
unless @entries.size.positive?
|
||||
@error_reason = "No entries to write."
|
||||
return false
|
||||
end
|
||||
|
||||
if File.directory?(@path)
|
||||
@error_reason = format("Path is a directory: %s", @path)
|
||||
return false
|
||||
end
|
||||
|
||||
File.open(@path, "wb") do |f|
|
||||
header = MixHeader.new(encrypted? ? MIX2_HEADER : MIX1_HEADER, 0, 0, 0)
|
||||
|
||||
# write mime type
|
||||
write_i32(f, header.mime_type)
|
||||
|
||||
f.pos = 16
|
||||
|
||||
# sort entries by crc32 of their name
|
||||
sort_entries
|
||||
|
||||
# write file blobs
|
||||
@entries.each do |entry|
|
||||
# store current io position
|
||||
pos = f.pos
|
||||
|
||||
# copy entry to stream
|
||||
entry.copy_to(f)
|
||||
|
||||
# update entry with new offset
|
||||
entry.info.content_offset = pos
|
||||
|
||||
# add alignment padding
|
||||
padding = (-f.pos & 7)
|
||||
padding.times do |i|
|
||||
write_u8(f, 0)
|
||||
end
|
||||
end
|
||||
|
||||
# Save file data offset
|
||||
header.file_data_offset = f.pos
|
||||
|
||||
# write number of entries
|
||||
write_i32(f, @entries.size)
|
||||
|
||||
# write entries file data
|
||||
@entries.each do |entry|
|
||||
write_u32(f, entry.info.crc32)
|
||||
write_u32(f, entry.info.content_offset)
|
||||
write_u32(f, entry.info.content_length)
|
||||
end
|
||||
|
||||
# save file names offset
|
||||
header.file_names_offset = f.pos
|
||||
|
||||
# write number of entries
|
||||
write_i32(f, @entries.size)
|
||||
|
||||
# write entry names
|
||||
@entries.each do |entry|
|
||||
write_string(f, entry.name)
|
||||
end
|
||||
|
||||
# jump to io_position 4
|
||||
f.pos = 4
|
||||
# write rest of header
|
||||
|
||||
write_i32(f, header.file_data_offset)
|
||||
write_i32(f, header.file_names_offset)
|
||||
write_i32(f, header._reserved)
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def valid?
|
||||
# ALL entries MUST have unique case-insensitive names
|
||||
@entries.each do |a|
|
||||
@entries.each do |b|
|
||||
next if a == b
|
||||
|
||||
return false if a.name.upcase == b.name.upcase
|
||||
end
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def encrypted?
|
||||
@encrypted
|
||||
end
|
||||
|
||||
def add_file(path:, replace: false)
|
||||
return false unless File.exist?(path)
|
||||
return false if File.directory?(path)
|
||||
|
||||
entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
|
||||
add_entry(entry: entry, replace: replace)
|
||||
end
|
||||
|
||||
def add_blob(path:, blob:, replace: false)
|
||||
info = EntryInfoHeader.new(0, 0, blob.size)
|
||||
entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
||||
into.crc32 = @entries.last.calculate_crc32
|
||||
|
||||
add_entry(entry: entry, replace: replace)
|
||||
end
|
||||
|
||||
def add_entry(entry:, replace: false)
|
||||
duplicate = @entries.find { |e| e.name.upcase == entry.name.upcase }
|
||||
|
||||
if duplicate
|
||||
if replace
|
||||
@entries.delete(duplicate)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
@entries << entry
|
||||
true
|
||||
end
|
||||
|
||||
def sort_entries
|
||||
return false if @entries.any? { |e| e.info.crc32 == 0 }
|
||||
|
||||
@entries.sort! { |a, b| a.info.crc32 <=> b.info.crc32 }
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def read_i32(f) = f.read(4).unpack1("l")
|
||||
def read_u32(f) = f.read(4).unpack1("L")
|
||||
def read_u8(f) = f.read(1).unpack1("c")
|
||||
def read_string(f)
|
||||
f.read(read_u8(f)).strip
|
||||
end
|
||||
|
||||
def write_i32(f, value) = f.write([value].pack("l"))
|
||||
def write_u32(f, value) = f.write([value].pack("L"))
|
||||
def write_u8(f, value) = f.write([value].pack("c"))
|
||||
def write_string(f, string)
|
||||
length = string.size + 1 # include null byte
|
||||
write_u8(f, length)
|
||||
f.write(string)
|
||||
write_u8(f, 0) # null byte
|
||||
end
|
||||
end
|
||||
end
|
||||
|
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 |
BIN
screenshots/screenshot-games.webp
Normal file
|
After Width: | Height: | Size: 73 KiB |
@@ -14,16 +14,28 @@ require "logger"
|
||||
require "time"
|
||||
require "base64"
|
||||
require "zip"
|
||||
require "async"
|
||||
require "async/http/endpoint"
|
||||
require "async/websocket/client"
|
||||
require "async/http/internet/instance"
|
||||
|
||||
class W3DHub
|
||||
W3DHUB_DEBUG = ARGV.join.include?("--debug")
|
||||
W3DHUB_DEVELOPER = ARGV.join.include?("--developer")
|
||||
|
||||
GAME_ROOT_PATH = File.expand_path(".", __dir__)
|
||||
CACHE_PATH = "#{GAME_ROOT_PATH}/data/cache"
|
||||
SETTINGS_FILE_PATH = "#{GAME_ROOT_PATH}/data/settings.json"
|
||||
# Use the real working directory as the root for runtime data/logs
|
||||
GAME_ROOT_PATH = Dir.pwd
|
||||
|
||||
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 = W3DHUB_DEBUG ? Logger.new(STDOUT) : Logger.new("#{LOGS_PATH}/w3d_hub_linux_launcher.log", "daily")
|
||||
LOGGER.level = Logger::Severity::DEBUG # W3DHUB_DEBUG ? Logger::Severity::DEBUG : Logger::Severity::WARN
|
||||
|
||||
LOG_TAG = "W3DHubLinuxLauncher"
|
||||
@@ -75,17 +87,14 @@ class W3DHub
|
||||
BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff")
|
||||
end
|
||||
|
||||
require "i18n"
|
||||
require "websocket-client-simple"
|
||||
require "English"
|
||||
require "sdl2"
|
||||
|
||||
require_relative "lib/i18n"
|
||||
I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"]
|
||||
I18n.default_locale = :en
|
||||
|
||||
# GUI_DEBUG = true
|
||||
require_relative "lib/gui_state_ext"
|
||||
|
||||
require_relative "lib/win32_stub" unless Gem.win_platform?
|
||||
|
||||
require_relative "lib/version"
|
||||
@@ -95,11 +104,12 @@ require_relative "lib/store"
|
||||
require_relative "lib/window"
|
||||
require_relative "lib/cache"
|
||||
require_relative "lib/settings"
|
||||
require_relative "lib/mixer"
|
||||
require_relative "lib/ww_mix"
|
||||
require_relative "lib/ico"
|
||||
require_relative "lib/multicast_server"
|
||||
require_relative "lib/broadcast_server"
|
||||
require_relative "lib/hardware_survey"
|
||||
require_relative "lib/game_settings"
|
||||
require_relative "lib/websocket_client"
|
||||
require_relative "lib/background_worker"
|
||||
require_relative "lib/application_manager"
|
||||
require_relative "lib/application_manager/manifest"
|
||||
@@ -110,7 +120,6 @@ require_relative "lib/application_manager/tasks/installer"
|
||||
require_relative "lib/application_manager/tasks/updater"
|
||||
require_relative "lib/application_manager/tasks/uninstaller"
|
||||
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/boot"
|
||||
require_relative "lib/states/interface"
|
||||
@@ -121,6 +130,8 @@ require_relative "lib/states/dialogs/prompt_dialog"
|
||||
require_relative "lib/states/dialogs/confirm_dialog"
|
||||
require_relative "lib/states/dialogs/direct_connect_dialog"
|
||||
require_relative "lib/states/dialogs/game_settings_dialog"
|
||||
require_relative "lib/states/dialogs/import_game_dialog"
|
||||
require_relative "lib/states/dialogs/launcher_updater_dialog"
|
||||
|
||||
require_relative "lib/api"
|
||||
require_relative "lib/api/service_status"
|
||||
@@ -150,7 +161,18 @@ require_relative "lib/asterisk/states/game_form"
|
||||
require_relative "lib/asterisk/states/irc_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}" }
|
||||
|
||||
|
||||