25 Commits

Author SHA1 Message Date
d630e5044e Make clear package cache button functional 2026-01-14 08:12:29 -06:00
632fc2c05c Bump version 2026-01-09 09:59:51 -06:00
bf8f440ec7 Update gems 2026-01-09 09:37:24 -06:00
633aa10d4a Fixed ServerBrowser differed recalcuate after server update not implemented correctly :D 2026-01-09 09:26:45 -06:00
51d6d981f1 Removed implementation of find_element_by_tag from ServerBrowser, is implemented in CyberarmEngine nowadays. 2026-01-09 09:10:36 -06:00
820da31fe2 Remove Excon ca bundle setting since we removed Excon 2026-01-09 09:02:41 -06:00
6281a44961 Fixed crash 2026-01-09 09:01:55 -06:00
90a1c47389 Fix typo avorive -> avorite, improve Play Now server selection to check server version if provided 2026-01-08 21:18:43 -06:00
782d0f1cb3 Replace websocket-client-simple gem with async-websocket 2026-01-08 21:17:00 -06:00
f2cd26dda3 Update readme 2026-01-07 18:17:23 -06:00
11e5b578a1 Added screenshot for README 2026-01-07 18:12:21 -06:00
7f7e0fab6a Replace usage of Mixer with WWMix reimplementation, fixes excessive memory usage by NOT eager loading MIX file data blobs 2026-01-07 18:05:02 -06:00
a0ff6ec812 Reimplemented MIX file reader/writer 2026-01-07 17:05:40 -06:00
840bc849d3 Migrated away from Excon and to async-http, fixes issues with ipv6 dns resolving but not reachable- and is the start towards more migration to async libs, websocket based server list updater is temporarily broken 2026-01-07 14:18:48 -06:00
752bd2b026 Show server version in server browser 2026-01-07 00:16:14 -06:00
8086ab59b9 Renamed multicast server to broadcast server 2026-01-07 00:15:54 -06:00
948fcfda9a Use correct repo url for self-updater (doesn't work yet, but its a step) 2026-01-06 23:26:56 -06:00
daceb5d56d Use application installed version 2026-01-06 23:26:56 -06:00
e6eae4117f Use server version 2026-01-06 23:26:56 -06:00
a8c74095fe Show launcher version in title bar 2026-01-06 23:26:56 -06:00
f608f45f02 Formatted boot, rescue from write error at start up in #app_icons 2025-12-30 17:27:45 -06:00
603328a51f Update gems, drop i18n gem and implement basic replacement 2025-12-11 16:23:39 -06:00
48297ad9cd Update README 2025-11-30 21:31:57 -06:00
39fbb9df38 Add FUNDING.yml 2025-11-29 11:40:04 -06:00
bc9a524a55 Handle edge case where Gosu.user_languages is empty (on alpine linux for example) 2025-11-29 11:31:36 -06:00
25 changed files with 853 additions and 725 deletions

15
.github/FUNDING.yml vendored Normal file
View 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']

View File

@@ -2,7 +2,7 @@ name: Build Launcher Binary
on: on:
push: push:
branches: [ master, test ] branches: [master]
workflow_dispatch: workflow_dispatch:
jobs: jobs:

17
Gemfile
View File

@@ -1,16 +1,15 @@
source "https://rubygems.org" source "https://rubygems.org"
gem "base64" gem "base64"
gem "excon" gem "async-http"
gem "async-websocket"
gem "cyberarm_engine" gem "cyberarm_engine"
gem "sdl2-bindings" gem "sdl2-bindings"
gem "libui", platforms: [:windows] gem "libui", platforms: [:windows]
gem "digest-crc" gem "digest-crc"
gem "i18n"
gem "ircparser" gem "ircparser"
gem "rexml" gem "rexml"
gem "rubyzip" gem "rubyzip"
gem "websocket-client-simple"
gem "win32-process", platforms: [:windows] gem "win32-process", platforms: [:windows]
gem "win32-security", platforms: [:windows] gem "win32-security", platforms: [:windows]
@@ -19,9 +18,9 @@ gem "win32-security", platforms: [:windows]
# use `bundle _x.y.z_ COMMAND` to use this one... # use `bundle _x.y.z_ COMMAND` to use this one...
# NOTE: Releasy needs to be installed as a system gem i.e. `rake install` # NOTE: Releasy needs to be installed as a system gem i.e. `rake install`
# NOTE: contents of the `gemhome` folder in the packaged folder need to be moved into the lib/ruby/gems\<RUBY_VERSION> folder # NOTE: contents of the `gemhome` folder in the packaged folder need to be moved into the lib/ruby/gems\<RUBY_VERSION> folder
# group :windows_packaging do group :windows_packaging do
# gem "bundler", "~>2.4.3" gem "bundler", "~>2.4.3"
# gem "rake" gem "rake"
# gem "ocran" gem "ocran"
# gem "releasy"#, path: "../releasy" gem "releasy"#, path: "../releasy"
# end end

View File

@@ -1,39 +1,85 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
async (2.35.1)
console (~> 1.29)
fiber-annotation
io-event (~> 1.11)
metrics (~> 0.12)
traces (~> 0.18)
async-http (0.93.0)
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.1)
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) base64 (0.3.0)
concurrent-ruby (1.3.5) console (1.34.2)
cyberarm_engine (0.24.5) fiber-annotation
fiber-local (~> 1.1)
json
cri (2.15.12)
cyberarm_engine (0.25.0)
gosu (~> 1.1) gosu (~> 1.1)
digest-crc (0.7.0) digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0) rake (>= 12.0.0, < 14.0.0)
event_emitter (0.2.6) ffi (1.17.0)
excon (1.3.0) ffi-win32-extensions (1.1.0)
logger ffi (>= 1.15.5, <= 1.17.0)
ffi (1.17.2-x64-mingw-ucrt) fiber-annotation (0.2.0)
ffi (1.17.2-x86_64-linux-gnu) fiber-local (1.1.0)
ffi-win32-extensions (1.0.4) fiber-storage
ffi fiber-storage (1.0.1)
fiddle (1.1.8) fiddle (1.1.8)
gosu (1.4.6) gosu (1.4.6)
i18n (1.14.7) io-endpoint (0.16.0)
concurrent-ruby (~> 1.0) io-event (1.14.2)
io-stream (0.11.1)
ircparser (1.0.0) ircparser (1.0.0)
json (2.18.0)
libui (0.2.0-x64-mingw-ucrt) libui (0.2.0-x64-mingw-ucrt)
fiddle fiddle
logger (1.7.0) metrics (0.15.0)
mutex_m (0.3.0) ocran (1.3.17)
rake (13.3.0) fiddle (~> 1.0)
protocol-hpack (1.5.1)
protocol-http (0.58.0)
protocol-http1 (0.36.0)
protocol-http (~> 0.58)
protocol-http2 (0.23.0)
protocol-hpack (~> 1.4)
protocol-http (~> 0.47)
protocol-rack (0.21.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.4)
rake (13.3.1)
releasy (0.2.4)
bundler (>= 1.2.1)
cri (~> 2.15.0)
ocran (~> 1.3.0)
rake (>= 0.9.2.2)
rexml (3.4.4) rexml (3.4.4)
rubyzip (3.1.1) rubyzip (3.2.2)
sdl2-bindings (0.2.3) sdl2-bindings (0.2.3)
ffi (~> 1.15) ffi (~> 1.15)
websocket (1.2.11) traces (0.18.2)
websocket-client-simple (0.9.0)
base64
event_emitter
mutex_m
websocket
win32-process (0.10.0) win32-process (0.10.0)
ffi (>= 1.0.0) ffi (>= 1.0.0)
win32-security (0.5.0) win32-security (0.5.0)
@@ -42,22 +88,24 @@ GEM
PLATFORMS PLATFORMS
x64-mingw-ucrt x64-mingw-ucrt
x86_64-linux
DEPENDENCIES DEPENDENCIES
async-http
async-websocket
base64 base64
bundler (~> 2.4.3)
cyberarm_engine cyberarm_engine
digest-crc digest-crc
excon
i18n
ircparser ircparser
libui libui
ocran
rake
releasy
rexml rexml
rubyzip rubyzip
sdl2-bindings sdl2-bindings
websocket-client-simple
win32-process win32-process
win32-security win32-security
BUNDLED WITH BUNDLED WITH
2.6.8 2.4.22

View File

@@ -1,14 +1,21 @@
![screenshot](https://raw.githubusercontent.com/cyberarm/w3d_hub_linux_launcher/development/screenshots/screenshot-games.webp)
# Cyberarm's Linux Friendly W3D Hub Launcher # 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. 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. Only requires OpenGL, Ruby, and a few gems.
## Installing ## Download
* Install Ruby 3.0+, from your package manager. [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 Gosu's [dependencies](https://github.com/gosu/gosu/wiki/Getting-Started-on-Linux).
* Install required gems: `bundle install` * Install required gems: `bundle install`
## Usage ### Usage
`ruby w3d_hub_linux_launcher.rb` `ruby w3d_hub_linux_launcher.rb`
## Contributing ### Contributing
Contributors welcome, especially if anyone can lend a hand at reducing patching memory usage. Contributors welcome.

View File

@@ -1,44 +1,41 @@
class W3DHub class W3DHub
class Api class Api
# Set Excon default CA file if found
if (ca_file = W3DHub.ca_bundle_path)
Excon.defaults[:ssl_ca_file] = ca_file
end
LOG_TAG = "W3DHub::Api".freeze LOG_TAG = "W3DHub::Api".freeze
API_TIMEOUT = 30 # seconds API_TIMEOUT = 30 # seconds
USER_AGENT = "Cyberarm's Linux Friendly W3D Hub Launcher v#{W3DHub::VERSION}".freeze USER_AGENT = "Cyberarm's Linux Friendly W3D Hub Launcher v#{W3DHub::VERSION}".freeze
DEFAULT_HEADERS = { DEFAULT_HEADERS = [
"User-Agent": USER_AGENT, ["user-agent", USER_AGENT],
"Accept": "application/json" ["accept", "application/json"]
}.freeze ].freeze
FORM_ENCODED_HEADERS = { FORM_ENCODED_HEADERS = [
"User-Agent": USER_AGENT, ["user-agent", USER_AGENT],
"Accept": "application/json", ["accept", "application/json"],
"Content-Type": "application/x-www-form-urlencoded" ["content-type", "application/x-www-form-urlencoded"]
}.freeze ].freeze
def self.on_thread(method, *args, &callback) def self.on_thread(method, *args, &callback)
BackgroundWorker.foreground_job(-> { Api.send(method, *args) }, callback) BackgroundWorker.foreground_job(-> { Api.send(method, *args) }, callback)
end end
class DummyResponse class Response
def initialize(error) def initialize(error: nil, status: -1, body: "")
@status = status
@body = body
@error = error @error = error
end end
def success? def success?
false @status == 200
end end
def status def status
-1 @status
end end
def body def body
"" @body
end end
def error def error
@@ -48,103 +45,60 @@ class W3DHub
#! === W3D Hub API === !# #! === W3D Hub API === !#
W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze #
W3DHUB_API_CONNECTION = Excon.new(W3DHUB_API_ENDPOINT, persistent: true)
ALT_W3DHUB_API_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # ALT_W3DHUB_API_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze #
ALT_W3DHUB_API_API_CONNECTION = Excon.new(ALT_W3DHUB_API_ENDPOINT, persistent: true)
def self.excon(method, url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.async_http(method, url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
case backend case backend
when :w3dhub when :w3dhub
connection = W3DHUB_API_CONNECTION
endpoint = W3DHUB_API_ENDPOINT endpoint = W3DHUB_API_ENDPOINT
when :alt_w3dhub when :alt_w3dhub
connection = ALT_W3DHUB_API_API_CONNECTION
endpoint = ALT_W3DHUB_API_ENDPOINT endpoint = ALT_W3DHUB_API_ENDPOINT
when :gsh when :gsh
connection = GSH_CONNECTION
endpoint = SERVER_LIST_ENDPOINT endpoint = SERVER_LIST_ENDPOINT
end end
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{endpoint}#{url}\"..." } url = "#{endpoint}#{url}" unless url.start_with?("http")
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
# Inject Authorization header if account data is populated # Inject Authorization header if account data is populated
if Store.account if Store.account
logger.debug(LOG_TAG) { " Injecting Authorization header..." } logger.debug(LOG_TAG) { " Injecting Authorization header..." }
headers = headers.dup headers = headers.dup
headers["Authorization"] = "Bearer #{Store.account.access_token}" headers << ["authorization", "Bearer #{Store.account.access_token}"]
end end
Sync do
begin begin
connection.send( response = Async::HTTP::Internet.send(method, url, headers, body)
method,
path: url.sub(endpoint, ""), Response.new(status: response.status, body: response.read)
headers: headers, rescue Async::TimeoutError => e
body: body,
nonblock: true,
tcp_nodelay: true,
write_timeout: API_TIMEOUT,
read_timeout: API_TIMEOUT,
connect_timeout: API_TIMEOUT,
idempotent: true,
retry_limit: 3,
retry_interval: 1,
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
)
rescue Excon::Error::Timeout => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" } logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
DummyResponse.new(e) Response.new(error: e)
rescue Excon::Error => e rescue StandardError => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" } logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
logger.error(LOG_TAG) { e } logger.error(LOG_TAG) { e }
DummyResponse.new(e) Response.new(error: e)
ensure
response&.close
end
end end
end end
def self.post(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.post(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
excon(:post, url, headers, body, backend) async_http(:post, url, headers, body, backend)
end end
def self.get(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.get(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
excon(:get, url, headers, body, backend) async_http(:get, url, headers, body, backend)
end end
# Api.get but handles any URL instead of known hosts # Api.get but handles any URL instead of known hosts
def self.fetch(url, headers = DEFAULT_HEADERS, body = nil, backend = nil) def self.fetch(url, headers = DEFAULT_HEADERS, body = nil, backend = nil)
uri = URI(url) async_http(:get, url, headers, body, backend)
# Use Api.get for `W3DHUB_API_ENDPOINT` URL's to exploit keep alive and connection reuse (faster responses)
return excon(:get, url, headers, body, backend) if "#{uri.scheme}://#{uri.host}" == W3DHUB_API_ENDPOINT
logger.debug(LOG_TAG) { "Fetching GET \"#{url}\"..." }
begin
Excon.get(
url,
headers: headers,
body: body,
nonblock: true,
tcp_nodelay: true,
write_timeout: API_TIMEOUT,
read_timeout: API_TIMEOUT,
connect_timeout: API_TIMEOUT,
idempotent: true,
retry_limit: 3,
retry_interval: 1,
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
)
rescue Excon::Error::Timeout => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
DummyResponse.new(e)
rescue Excon::Error => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
logger.error(LOG_TAG) { e }
DummyResponse.new(e)
end
end end
# Method: POST # Method: POST
@@ -163,7 +117,7 @@ class W3DHub
# On a failed login the service responds with: # On a failed login the service responds with:
# {"error":"login-failed"} # {"error":"login-failed"}
def self.refresh_user_login(refresh_token, backend = :w3dhub) def self.refresh_user_login(refresh_token, backend = :w3dhub)
body = "data=#{JSON.dump({refreshToken: refresh_token})}" body = URI.encode_www_form("data": JSON.dump({refreshToken: refresh_token}))
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend)
if response.status == 200 if response.status == 200
@@ -183,7 +137,7 @@ class W3DHub
# See #user_refresh_token # See #user_refresh_token
def self.user_login(username, password, backend = :w3dhub) def self.user_login(username, password, backend = :w3dhub)
body = "data=#{JSON.dump({username: username, password: password})}" body = URI.encode_www_form("data": JSON.dump({username: username, password: password}))
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend)
if response.status == 200 if response.status == 200
@@ -205,7 +159,7 @@ class W3DHub
# #
# Response: avatar-uri (Image download uri), id, username # Response: avatar-uri (Image download uri), id, username
def self.user_details(id, backend = :w3dhub) def self.user_details(id, backend = :w3dhub)
body = "data=#{JSON.dump({ id: id })}" body = URI.encode_www_form("data": JSON.dump({ id: id }))
user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend) user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend)
if user_details.status == 200 if user_details.status == 200
@@ -322,7 +276,7 @@ class W3DHub
# Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates) # Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates)
# Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one # Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one
def self.news(category, backend = :w3dhub) def self.news(category, backend = :w3dhub)
body = "data=#{JSON.dump({category: category})}" body = URI.encode_www_form("data": JSON.dump({category: category}))
response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend) response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend)
if response.status == 200 if response.status == 200
@@ -383,7 +337,6 @@ class W3DHub
# SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze # SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze
SERVER_LIST_ENDPOINT = "https://gsh.w3d.cyberarm.dev".freeze SERVER_LIST_ENDPOINT = "https://gsh.w3d.cyberarm.dev".freeze
# SERVER_LIST_ENDPOINT = "http://127.0.0.1:9292".freeze # SERVER_LIST_ENDPOINT = "http://127.0.0.1:9292".freeze
GSH_CONNECTION = Excon.new(SERVER_LIST_ENDPOINT, persistent: true)
# Method: GET # Method: GET
# FORMAT: JSON # FORMAT: JSON

View File

@@ -2,8 +2,9 @@ class W3DHub
class Api class Api
class ServerListServer class ServerListServer
NO_OR_BAD_PING = 1_000_000 NO_OR_BAD_PING = 1_000_000
NO_OR_DEFAULT_VERSION = "838"
attr_reader :id, :game, :address, :port, :region, :channel, :ping, :status attr_reader :id, :game, :address, :port, :region, :channel, :version, :ping, :status
def initialize(hash) def initialize(hash)
@data = hash @data = hash
@@ -14,6 +15,7 @@ class W3DHub
@port = @data[:port] @port = @data[:port]
@region = @data[:region] @region = @data[:region]
@channel = @data[:channel] || "release" @channel = @data[:channel] || "release"
@version = @data[:version] || NO_OR_DEFAULT_VERSION
@ping = NO_OR_BAD_PING @ping = NO_OR_BAD_PING
@status = Status.new(@data[:status]) @status = Status.new(@data[:status])

View File

@@ -2,6 +2,9 @@ class W3DHub
class Api class Api
class ServerListUpdater class ServerListUpdater
LOG_TAG = "W3DHub::Api::ServerListUpdater".freeze LOG_TAG = "W3DHub::Api::ServerListUpdater".freeze
TYPE_PING = 6
include CyberarmEngine::Common include CyberarmEngine::Common
@@instance = nil @@instance = nil
@@ -15,6 +18,7 @@ class W3DHub
def initialize def initialize
@auto_reconnect = false @auto_reconnect = false
@reconnection_delay = 1
@invocation_id = 0 @invocation_id = 0
@@ -24,12 +28,13 @@ class W3DHub
def run def run
Thread.new do Thread.new do
Sync do |task|
begin begin
connect @auto_reconnect = true
while W3DHub::BackgroundWorker.alive? while W3DHub::BackgroundWorker.alive?
connect if @auto_reconnect connect if @auto_reconnect
sleep 1 sleep @reconnection_delay
end end
rescue => e rescue => e
puts e puts e
@@ -39,6 +44,7 @@ class W3DHub
retry retry
end end
end end
end
logger.debug(LOG_TAG) { "Cleaning up..." } logger.debug(LOG_TAG) { "Cleaning up..." }
@@instance = nil @@instance = nil
@@ -52,9 +58,13 @@ class W3DHub
if response.status != 200 if response.status != 200
@auto_reconnect = true @auto_reconnect = true
@reconnection_delay = @reconnection_delay * 2
@reconnection_delay = 60 if @reconnection_delay > 60
return return
end end
@reconnection_delay = 1
data = JSON.parse(response.body, symbolize_names: true) data = JSON.parse(response.body, symbolize_names: true)
@invocation_id = 0 if @invocation_id > 9095 @invocation_id = 0 if @invocation_id > 9095
@@ -63,7 +73,7 @@ class W3DHub
logger.debug(LOG_TAG) { "Connecting to websocket..." } logger.debug(LOG_TAG) { "Connecting to websocket..." }
this = self this = self
@ws = 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 ws.on(:open) do
logger.debug(LOG_TAG) { "Requesting json protocol, v1..." } logger.debug(LOG_TAG) { "Requesting json protocol, v1..." }
ws.send({ protocol: "json", version: 1 }.to_json + "\x1e") ws.send({ protocol: "json", version: 1 }.to_json + "\x1e")
@@ -78,16 +88,18 @@ class W3DHub
end end
ws.on(:message) do |msg| 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) hash = JSON.parse(msg, symbolize_names: true)
# pp hash if hash[:target] != "ServerStatusChanged" && hash[:type] != 6 && hash[:type] != 3 # pp hash if hash[:target] != "ServerStatusChanged" && hash[:type] != 6 && hash[:type] != 3
# Send PING(?) # Send PING(?)
if hash.empty? || hash[:type] == 6 if hash.empty? || hash[:type] == TYPE_PING
ws.send({ type: 6 }.to_json + "\x1e") ws.send({ type: TYPE_PING }.to_json + "\x1e")
else next
end
case hash[:type] case hash[:type]
when 1 when 1
case hash[:target] case hash[:target]
@@ -95,7 +107,12 @@ class W3DHub
data = hash[:arguments].first data = hash[:arguments].first
this.invocation_id += 1 this.invocation_id += 1
out = { "type": 1, "invocationId": "#{this.invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [data[:id], 1] } out = {
"type": 1,
"invocationId": "#{this.invocation_id}",
"target": "SubscribeToServerStatusUpdates",
"arguments": [data[:id], 1]
}
ws.send(out.to_json + "\x1e") ws.send(out.to_json + "\x1e")
BackgroundWorker.foreground_job( BackgroundWorker.foreground_job(
@@ -133,10 +150,9 @@ class W3DHub
end end
end end
end end
end
ws.on(:close) do |e| ws.on(:close) do
logger.error(LOG_TAG) { e } logger.error(LOG_TAG) { "Connection closed." }
this.auto_reconnect = true this.auto_reconnect = true
ws.close ws.close
end end
@@ -181,14 +197,24 @@ class W3DHub
# unsubscribe from removed servers # unsubscribe from removed servers
removed_servers.each do removed_servers.each do
@invocation_id += 1 @invocation_id += 1
out = { "type": 1, "invocationId": "#{@invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, 0] } out = {
"type": 1,
"invocationId": "#{@invocation_id}",
"target": "SubscribeToServerStatusUpdates",
"arguments": [server.id, 0]
}
ws.send(out.to_json + "\x1e") ws.send(out.to_json + "\x1e")
end end
# subscribe to new servers # subscribe to new servers
new_servers.each do new_servers.each do
@invocation_id += 1 @invocation_id += 1
out = { "type": 1, "invocationId": "#{@invocation_id}", "target": "SubscribeToServerStatusUpdates", "arguments": [server.id, 1] } out = {
"type": 1,
"invocationId": "#{@invocation_id}",
"target": "SubscribeToServerStatusUpdates",
"arguments": [server.id, 1]
}
ws.send(out.to_json + "\x1e") ws.send(out.to_json + "\x1e")
end end
end end

View File

@@ -249,9 +249,17 @@ class W3DHub
return nil unless app_data return nil unless app_data
found_server = Store.server_list.select do |server| 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 server.game == app_id &&
end&.first server.channel == channel &&
!server.status.password &&
server.status.player_count < server.status.max_players
end
# 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 ? found_server : nil found_server ? found_server : nil
end end
@@ -283,7 +291,7 @@ class W3DHub
end end
end end
def favorive(app_id, bool) def favorite(app_id, bool)
Store.settings[:favorites] ||= {} Store.settings[:favorites] ||= {}
if bool if bool

View File

@@ -739,34 +739,51 @@ class W3DHub
temp_file_path = normalize_path(manifest_file.name, temp_path) temp_file_path = normalize_path(manifest_file.name, temp_path)
logger.info(LOG_TAG) { " Loading #{temp_file_path}.patch..." } logger.info(LOG_TAG) { " Loading #{temp_file_path}.patch..." }
patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_file_path}.patch", ignore_crc_mismatches: false) patch_mix = W3DHub::WWMix.new(path: "#{temp_file_path}.patch")
patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name.casecmp?(".w3dhub.patch") || f.name.casecmp?(".bhppatch") }.data, symbolize_names: true) 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
patch_info = JSON.parse(patch_entry.blob, symbolize_names: true)
logger.info(LOG_TAG) { " Loading #{file_path}..." } logger.info(LOG_TAG) { " Loading #{file_path}..." }
target_mix = W3DHub::Mixer::Reader.new(file_path: "#{file_path}", ignore_crc_mismatches: false) 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? logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive?
patch_info[:removedFiles].each do |file| patch_info[:removedFiles].each do |file|
logger.debug(LOG_TAG) { " #{file}" } logger.debug(LOG_TAG) { " #{file}" }
target_mix.package.files.delete_if { |f| f.name.casecmp?(file) } target_mix.entries.delete_if { |e| e.name.casecmp?(file) }
end end
logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive? logger.info(LOG_TAG) { " Adding/Updating files..." } if patch_info[:updatedFiles].size.positive?
patch_info[:updatedFiles].each do |file| patch_info[:updatedFiles].each do |file|
logger.debug(LOG_TAG) { " #{file}" } logger.debug(LOG_TAG) { " #{file}" }
patch = patch_mix.package.files.find { |f| f.name.casecmp?(file) } patch = patch_mix.entries.find { |e| e.name.casecmp?(file) }
target = target_mix.package.files.find { |f| f.name.casecmp?(file) } target = target_mix.entries.find { |e| e.name.casecmp?(file) }
if target if target
target_mix.package.files[target_mix.package.files.index(target)] = patch target_mix.entries[target_mix.entries.index(target)] = patch
else else
target_mix.package.files << patch target_mix.entries << patch
end end
end end
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive? logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
W3DHub::Mixer::Writer.new(file_path: "#{file_path}", package: target_mix.package, memory_buffer: true, encrypted: target_mix.encrypted?) temp_mix_path = "#{temp_path}/#{File.basename(file_path)}"
temp_mix = W3DHub::WWMix.new(path: temp_mix_path)
target_mix.entries.each { |e| temp_mix.add_entry(entry: e) }
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) FileUtils.remove_dir(temp_path)

View File

@@ -1,7 +1,7 @@
class W3DHub class W3DHub
# Maybe add remote game launch from server list app? # Maybe add remote game launch from server list app?
class MulticastServer # Maybe add intranet package delivery?
MULTICAST_ADDR = "224.87.51.68" class BroadcastServer
PORT = 7050 PORT = 7050
def initialize def initialize

View File

@@ -50,54 +50,16 @@ class W3DHub
end end
# Download a W3D Hub package # 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)
path = package_path(package.category, package.subcategory, package.name, package.version)
headers = Api::FORM_ENCODED_HEADERS
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 = Api::FORM_ENCODED_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 })}"
response = Api.post("/apis/launcher/1/get-package", headers, body)
total_bytes = package.size
remaining_bytes = total_bytes - start_from_bytes
response.each do |chunk|
file.write(chunk)
remaining_bytes -= chunk.size
block.call(chunk, remaining_bytes, total_bytes)
end
response.status == 200
ensure
file&.close
end
# Download a W3D Hub package
def self.fetch_package(package, block)
endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package" endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package"
if package.download_url if package.download_url
uri_path = package.download_url.split("/").last uri_path = package.download_url.split("/").last
endpoint_download_url = package.download_url.sub(uri_path, URI.encode_uri_component(uri_path)) endpoint_download_url = package.download_url.sub(uri_path, URI.encode_uri_component(uri_path))
end end
path = package_path(package.category, package.subcategory, package.name, package.version) path = package_path(package.category, package.subcategory, package.name, package.version)
headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT } headers = [["content-type", "application/x-www-form-urlencoded"], ["user-agent", Api::USER_AGENT]]
headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account && !package.download_url headers << ["authorization", "Bearer #{Store.account.access_token}"] if Store.account && !package.download_url
body = "data=#{JSON.dump({ category: package.category, subcategory: package.subcategory, name: package.name, version: package.version })}" 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 start_from_bytes = package.custom_partially_valid_at_bytes
logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" } logger.info(LOG_TAG) { " Start from bytes: #{start_from_bytes} of #{package.size}" }
@@ -107,54 +69,63 @@ class W3DHub
file = File.open(path, start_from_bytes.positive? ? "r+b" : "wb") file = File.open(path, start_from_bytes.positive? ? "r+b" : "wb")
if start_from_bytes.positive? if start_from_bytes.positive?
headers["Range"] = "bytes=#{start_from_bytes}-" headers << ["range", "bytes=#{start_from_bytes}-"]
file.pos = start_from_bytes file.pos = start_from_bytes
end end
streamer = lambda do |chunk, remaining_bytes, total_bytes| result = false
Sync do
response = nil
Async::HTTP::Internet.send(package.download_url ? :get : :post, endpoint_download_url, headers, body) do |r|
response = r
if r.success?
total_bytes = package.size
r.each do |chunk|
file.write(chunk) file.write(chunk)
block.call(chunk, remaining_bytes, total_bytes) block.call(chunk, total_bytes - file.pos, total_bytes)
end end
# Create a new connection due to some weirdness somewhere in Excon result = true
response = Excon.send( end
package.download_url ? :get : :post, end
endpoint_download_url,
tcp_nodelay: true,
headers: headers,
body: package.download_url ? "" : body,
chunk_size: 50_000,
response_block: streamer,
middlewares: Excon.defaults[:middlewares] + [Excon::Middleware::RedirectFollower]
)
if response.status == 200 || response.status == 206 if response.status == 200 || response.status == 206
return true result = true
else else
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" } logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" } logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false result = false
end end
rescue Excon::Error::Timeout => e 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) { " Connection to \"#{endpoint_download_url}\" timed out after: #{W3DHub::Api::API_TIMEOUT} seconds" }
logger.error(LOG_TAG) { e } 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) { " 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}" } logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false result = false
rescue Excon::Error => e rescue StandardError => e
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" errored:" } logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" errored:" }
logger.error(LOG_TAG) { e } 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) { " 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}" } logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
return false result = false
end
result
ensure ensure
file&.close file&.close
end end
# Download a W3D Hub package
def self.fetch_package(package, block)
async_fetch_package(package, block)
end
def self.acquire_net_lock(key) def self.acquire_net_lock(key)
Store["net_locks"] ||= {} Store["net_locks"] ||= {}

102
lib/i18n.rb Normal file
View 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

View File

@@ -1,386 +0,0 @@
require "digest"
require "stringio"
class W3DHub
# https://github.com/TheUnstoppable/MixLibrary used for reference
class Mixer
DEFAULT_BUFFER_SIZE = 32_000_000
MIX1_HEADER = 0x3158494D
MIX2_HEADER = 0x3258494D
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
@encrypted = false
# Valid header
if (mime = read_i32) && (mime == MIX1_HEADER || mime == MIX2_HEADER)
@encrypted = mime == MIX2_HEADER
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: Expected \"#{MIX1_HEADER}\" or \"#{MIX2_HEADER}\", got \"0x#{mime.to_s(16).upcase}\"\n(#{file_path})"
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
def encrypted?
@encrypted
end
end
class Writer
attr_reader :package
def initialize(file_path:, package:, memory_buffer: false, buffer_size: DEFAULT_BUFFER_SIZE, encrypted: false)
@package = package
@buffer = MemoryBuffer.new(file_path: file_path, mode: :write, buffer_size: buffer_size)
@buffer.pos = 0
@encrypted = encrypted
@buffer.write(encrypted? ? "MIX2" : "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
def encrypted?
@encrypted
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

View File

@@ -141,6 +141,8 @@ class W3DHub
stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do
if Store.application_manager.installed?(game.id, channel.id) 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.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.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? # hash[I18n.t(:"games.wine_configuration")] = { icon: "gear", block: proc { Store.application_manager.wine_configuration(game.id, channel.id) } } if W3DHub.unix?
@@ -367,7 +369,7 @@ class W3DHub
flow(width: 1.0, height: 28, padding: 8) do flow(width: 1.0, height: 28, padding: 8) do
para "Favorite", fill: true para "Favorite", fill: true
toggle_button checked: Store.application_manager.favorite?(game.id), height: 18, padding_top: 3, padding_right: 3, padding_bottom: 3, padding_left: 3 do |btn| toggle_button checked: Store.application_manager.favorite?(game.id), height: 18, padding_top: 3, padding_right: 3, padding_bottom: 3, padding_left: 3 do |btn|
Store.application_manager.favorive(game.id, btn.value) Store.application_manager.favorite(game.id, btn.value)
Store.settings.save_settings Store.settings.save_settings
populate_games_list populate_games_list

View File

@@ -3,7 +3,7 @@ class W3DHub
class ServerBrowser < Page class ServerBrowser < Page
def setup def setup
@server_locked_icons = {} @server_locked_icons = {}
@refresh_server_list = false @refresh_server_list_at_ms = nil
refresh_server = false refresh_server = false
@selected_server ||= nil @selected_server ||= nil
@@ -138,8 +138,8 @@ class W3DHub
def update def update
super super
if @refresh_server_list && Gosu.milliseconds >= @refresh_server_list if @refresh_server_list_at_ms && Gosu.milliseconds >= @refresh_server_list_at_ms
@refresh_server_list = nil @refresh_server_list_at_ms = nil
# populate_server_list # populate_server_list
reorder_server_list reorder_server_list
@@ -213,25 +213,13 @@ class W3DHub
server.ping == W3DHub::Api::ServerListServer::NO_OR_BAD_PING ? "Ping failed" : "Ping #{server.ping}ms" server.ping == W3DHub::Api::ServerListServer::NO_OR_BAD_PING ? "Ping failed" : "Ping #{server.ping}ms"
end end
def find_element_by_tag(container, tag, list = [])
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)
end
return list.first
end
def refresh_server_list(server, mode = :update) # :remove, :refresh_all def refresh_server_list(server, mode = :update) # :remove, :refresh_all
if mode == :refresh_all if mode == :refresh_all
populate_server_list populate_server_list
return return
end end
@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 @refresh_server = server if @selected_server&.id == server.id
server_container = find_element_by_tag(@server_list_container, server.id) server_container = find_element_by_tag(@server_list_container, server.id)
@@ -333,7 +321,8 @@ class W3DHub
para server&.status&.name, tag: :server_name, font: BOLD_FONT, text_wrap: :none para server&.status&.name, tag: :server_name, font: BOLD_FONT, text_wrap: :none
flow(width: 1.0, height: 1.0) do 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.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 para server.region, tag: :server_region
end end
end end
@@ -389,11 +378,12 @@ class W3DHub
flow(width: 1.0, height: 46, margin_top: 16, margin_bottom: 16) do flow(width: 1.0, height: 46, margin_top: 16, margin_bottom: 16) do
game_installed = Store.application_manager.installed?(server.game, server.channel) game_installed = Store.application_manager.installed?(server.game, server.channel)
game_updatable = Store.application_manager.updateable?(server.game, server.channel) game_updatable = Store.application_manager.updateable?(server.game, server.channel)
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) channel = Store.application_manager.channel(server.game, server.channel)
style = ((channel && channel.user_level.downcase.strip == "public") || server.channel == "release") ? {} : TESTING_BUTTON style = ((channel && channel.user_level.downcase.strip == "public") || server.channel == "release") ? {} : TESTING_BUTTON
flow(fill: true) flow(fill: true)
button "<b>#{I18n.t(:"server_browser.join_server")}</b>", enabled: (game_installed && !game_updatable), **style do button "<b>#{I18n.t(:"server_browser.join_server")}</b>", enabled: (game_installed && !game_updatable && matching_version), **style do
# Check for nickname # Check for nickname
# prompt for nickname # prompt for nickname
# !abort unless nickname set # !abort unless nickname set

View File

@@ -51,10 +51,18 @@ class W3DHub
end end
end end
stack(width: 128, max_height: 256, h_align: :center, margin_top: 16, fill: true) do flow(width: 256, height: 64, h_align: :center, margin_top: 16) do
button "Save", width: 1.0 do button "Save", width: 1.0 do
save_settings! save_settings!
end end
flow(fill: true)
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 end
end end

View File

@@ -2,7 +2,7 @@ class W3DHub
class Settings class Settings
def self.defaults def self.defaults
{ {
language: Gosu.user_languages.first.split("_").first, language: Gosu.user_languages.first&.split("_")&.first || "en",
app_install_dir: default_app_install_dir, app_install_dir: default_app_install_dir,
package_cache_dir: default_package_cache_dir, package_cache_dir: default_package_cache_dir,
parallel_downloads: 4, parallel_downloads: 4,

View File

@@ -25,7 +25,8 @@ class W3DHub
@task_index = 0 @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 stack(width: 1.0, fill: true) do
end end
@@ -41,7 +42,8 @@ class W3DHub
end end
def draw def draw
Gosu.draw_circle(window.width / 2, window.height / 2, @w3dhub_logo.width * (0.6 + Math.cos(Gosu.milliseconds / 1000.0 * Math::PI).abs * 0.05), 128, 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) @w3dhub_logo.draw_rot(window.width / 2, window.height / 2, 32)
super super
@@ -147,19 +149,18 @@ class W3DHub
} }
@status_label.value = "Checking uplink..." @status_label.value = "Checking uplink..."
domains.each do |key, value| domains.each do |key, _value|
begin
Resolv.getaddress(key.to_s) Resolv.getaddress(key.to_s)
rescue => e rescue StandardError => e
logger.error(LOG_TAG) {"Failed to resolve hostname: #{key.to_s}"} logger.error(LOG_TAG) { "Failed to resolve hostname: #{key}" }
logger.error(LOG_TAG) { e } logger.error(LOG_TAG) { e }
push_state( push_state(
ConfirmDialog, ConfirmDialog,
title: "DNS Resolution Failure", title: "DNS Resolution Failure",
message: "Failed to resolve: #{key.to_s}\n\nTry disabling VPN or proxy if in use.\n\n\nContinue offline?", message: "Failed to resolve: #{key}\n\nTry disabling VPN or proxy if in use.\n\n\nContinue offline?",
cancel_callback: ->() { window.close }, cancel_callback: -> { window.close },
accept_callback: ->() { accept_callback: lambda {
@offline_mode = true @offline_mode = true
Store.offline_mode = true Store.offline_mode = true
@tasks[:connectivity_check][:complete] = true @tasks[:connectivity_check][:complete] = true
@@ -169,7 +170,6 @@ class W3DHub
# Prevent task from being marked as completed # Prevent task from being marked as completed
return false return false
end end
end
@tasks[:connectivity_check][:complete] = true @tasks[:connectivity_check][:complete] = true
end end
@@ -187,7 +187,9 @@ class W3DHub
@tasks[:service_status][:complete] = true @tasks[:service_status][:complete] = true
else 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 @tasks[:service_status][:complete] = true
@offline_mode = true @offline_mode = true
@@ -199,7 +201,7 @@ class W3DHub
def launcher_updater def launcher_updater
@status_label.value = "Checking for Launcher updates..." # I18n.t(:"boot.checking_for_updates") @status_label.value = "Checking for Launcher updates..." # I18n.t(:"boot.checking_for_updates")
Api.on_thread(:fetch, "https://api.github.com/repos/Inq8/CAmod/releases/latest") do |response| Api.on_thread(:fetch, "https://api.github.com/repos/cyberarm/w3d_hub_linux_launcher/releases/latest") do |response|
if response.status == 200 if response.status == 200
hash = JSON.parse(response.body, symbolize_names: true) hash = JSON.parse(response.body, symbolize_names: true)
available_version = hash[:tag_name].downcase.sub("v", "") available_version = hash[:tag_name].downcase.sub("v", "")
@@ -246,6 +248,7 @@ class W3DHub
@status_label.value = "Retrieving application icons, this might take a moment..." # I18n.t(:"boot.checking_for_updates") @status_label.value = "Retrieving application icons, this might take a moment..." # I18n.t(:"boot.checking_for_updates")
packages = [] packages = []
failure = false
Store.applications.games.each do |app| Store.applications.games.each do |app|
packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" } packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" }
end end
@@ -261,21 +264,32 @@ class W3DHub
regenerate = false 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 if File.exist?(path) && !broken_or_out_dated_icon
regenerate = !File.exist?(generated_icon_path) regenerate = !File.exist?(generated_icon_path)
else else
begin
Cache.fetch_package(package, proc {}) Cache.fetch_package(package, proc {})
regenerate = true regenerate = true
end rescue Errno::EACCES => e
failure = true
if regenerate push_state(MessageDialog, title: "Fatal Error",
BackgroundWorker.foreground_job(-> { ICO.new(file: path) }, ->(result) { result.save(result.images.max_by(&:width), generated_icon_path) }) message: "Directory Permission Error (#{e.class}):\n#{e}.\n\nIs the required drive mounted?",
accept_callback: -> { window.close })
end end
end end
@tasks[:app_icons][:complete] = true 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 unless failure
end end
end end
@@ -296,7 +310,8 @@ class W3DHub
package_details&.each do |package| package_details&.each do |package|
next if package.error? next if package.error?
package_cache_path = Cache.package_path(package.category, package.subcategory, package.name, package.version) 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 missing_or_broken_image = File.exist?(package_cache_path) ? Digest::SHA256.new.hexdigest(File.binread(package_cache_path)).upcase != package.checksum.upcase : true
@@ -356,7 +371,7 @@ class W3DHub
"web-links": [], "web-links": [],
"extended-data": [ "extended-data": [
{ name: "colour", value: game[:colour] }, { name: "colour", value: game[:colour] },
{ name: "usesEngineCfg", value: game[:uses_engine_cfg] }, { name: "usesEngineCfg", value: game[:uses_engine_cfg] }
] ]
} }

View File

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

68
lib/websocket_client.rb Normal file
View File

@@ -0,0 +1,68 @@
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|
endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
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

View File

@@ -2,7 +2,7 @@ class W3DHub
class Window < CyberarmEngine::Window class Window < CyberarmEngine::Window
def setup def setup
self.show_stats_plotter = false self.show_stats_plotter = false
self.caption = I18n.t(:app_name) self.caption = "#{I18n.t(:app_name)} v#{VERSION}"
Store[:server_list] = [] Store[:server_list] = []
Store[:settings] = Settings.new Store[:settings] = Settings.new

280
lib/ww_mix.rb Normal file
View File

@@ -0,0 +1,280 @@
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:)
return false unless File.exist?(path)
return false if File.directory?(path)
info = EntryInfoHeader.new(0, 0, File.size(path))
@entries << Entry.new(name: File.basename(path), path: path, info: info)
true
end
def add_blob(path:, blob:)
info = EntryInfoHeader.new(0, 0, blob.size)
@entries << Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
into.crc32 = @entries.last.calculate_crc32
true
end
def add_entry(entry:)
@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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -14,7 +14,10 @@ require "logger"
require "time" require "time"
require "base64" require "base64"
require "zip" require "zip"
require "excon" require "async"
require "async/http/endpoint"
require "async/websocket/client"
require "async/http/internet/instance"
class W3DHub class W3DHub
W3DHUB_DEBUG = ARGV.join.include?("--debug") W3DHUB_DEBUG = ARGV.join.include?("--debug")
@@ -32,7 +35,7 @@ class W3DHub
FileUtils.mkdir_p(CACHE_PATH) unless Dir.exist?(CACHE_PATH) FileUtils.mkdir_p(CACHE_PATH) unless Dir.exist?(CACHE_PATH)
FileUtils.mkdir_p(LOGS_PATH) unless Dir.exist?(LOGS_PATH) FileUtils.mkdir_p(LOGS_PATH) unless Dir.exist?(LOGS_PATH)
LOGGER = Logger.new("#{LOGS_PATH}/w3d_hub_linux_launcher.log", "daily") 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 LOGGER.level = Logger::Severity::DEBUG # W3DHUB_DEBUG ? Logger::Severity::DEBUG : Logger::Severity::WARN
LOG_TAG = "W3DHubLinuxLauncher" LOG_TAG = "W3DHubLinuxLauncher"
@@ -84,11 +87,10 @@ class W3DHub
BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff") BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff")
end end
require "i18n"
require "websocket-client-simple"
require "English" require "English"
require "sdl2" require "sdl2"
require_relative "lib/i18n"
I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"] I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"]
I18n.default_locale = :en I18n.default_locale = :en
@@ -102,11 +104,12 @@ require_relative "lib/store"
require_relative "lib/window" require_relative "lib/window"
require_relative "lib/cache" require_relative "lib/cache"
require_relative "lib/settings" require_relative "lib/settings"
require_relative "lib/mixer" require_relative "lib/ww_mix"
require_relative "lib/ico" require_relative "lib/ico"
require_relative "lib/multicast_server" require_relative "lib/broadcast_server"
require_relative "lib/hardware_survey" require_relative "lib/hardware_survey"
require_relative "lib/game_settings" require_relative "lib/game_settings"
require_relative "lib/websocket_client"
require_relative "lib/background_worker" require_relative "lib/background_worker"
require_relative "lib/application_manager" require_relative "lib/application_manager"
require_relative "lib/application_manager/manifest" require_relative "lib/application_manager/manifest"