Compare commits
35 Commits
cc0910e68e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
|
|
e239f9cd4d | ||
|
|
b68d24deda | ||
|
|
1081832df0 | ||
|
|
c3cee78265 | ||
|
|
4d3163740a | ||
|
|
f1953c45e7 |
47
.github/workflows/build-tebako.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build Launcher Binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, test ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-tebako:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
architecture: [x64]
|
||||||
|
container:
|
||||||
|
image: ghcr.io/tamatebako/tebako-ubuntu-20.04:latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Gosu and native dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y libsdl2-dev libgl1-mesa-dev libopenal-dev libgmp-dev libfontconfig1-dev libsndfile1-dev libmpg123-dev libpango1.0-dev libtool libssl-dev libffi-dev
|
||||||
|
|
||||||
|
- name: Update Bundler and lockfile
|
||||||
|
run: |
|
||||||
|
gem install bundler -v 2.4.22
|
||||||
|
bundle _2.4.22_ lock --update --bundler
|
||||||
|
|
||||||
|
- name: Build Tebako binary
|
||||||
|
run: |
|
||||||
|
tebako press -P -R 3.4.1 -m bundle -o w3d_hub_linux_launcher -r $PWD -e w3d_hub_linux_launcher.rb
|
||||||
|
|
||||||
|
- name: Prepare artifact directory
|
||||||
|
run: |
|
||||||
|
mkdir w3d-hub-launcher-x86_64
|
||||||
|
cp w3d_hub_linux_launcher w3d-hub-launcher-x86_64/
|
||||||
|
cp -r media w3d-hub-launcher-x86_64/
|
||||||
|
cp -r locales w3d-hub-launcher-x86_64/
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: w3d-hub-launcher-x86_64
|
||||||
|
path: w3d-hub-launcher-x86_64
|
||||||
12
Gemfile.lock
@@ -8,22 +8,24 @@ GEM
|
|||||||
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)
|
event_emitter (0.2.6)
|
||||||
excon (1.2.7)
|
excon (1.3.0)
|
||||||
logger
|
logger
|
||||||
ffi (1.17.2-x64-mingw-ucrt)
|
ffi (1.17.2-x64-mingw-ucrt)
|
||||||
ffi (1.17.2-x86_64-linux-gnu)
|
ffi (1.17.2-x86_64-linux-gnu)
|
||||||
ffi-win32-extensions (1.0.4)
|
ffi-win32-extensions (1.0.4)
|
||||||
ffi
|
ffi
|
||||||
|
fiddle (1.1.8)
|
||||||
gosu (1.4.6)
|
gosu (1.4.6)
|
||||||
i18n (1.14.7)
|
i18n (1.14.7)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
ircparser (1.0.0)
|
ircparser (1.0.0)
|
||||||
libui (0.1.2-x64-mingw)
|
libui (0.2.0-x64-mingw-ucrt)
|
||||||
|
fiddle
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
rake (13.3.0)
|
rake (13.3.0)
|
||||||
rexml (3.4.1)
|
rexml (3.4.4)
|
||||||
rubyzip (2.4.1)
|
rubyzip (3.1.1)
|
||||||
sdl2-bindings (0.2.3)
|
sdl2-bindings (0.2.3)
|
||||||
ffi (~> 1.15)
|
ffi (~> 1.15)
|
||||||
websocket (1.2.11)
|
websocket (1.2.11)
|
||||||
@@ -58,4 +60,4 @@ DEPENDENCIES
|
|||||||
win32-security
|
win32-security
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.6.7
|
2.6.8
|
||||||
|
|||||||
20
lib/api.rb
@@ -1,5 +1,11 @@
|
|||||||
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
|
||||||
@@ -85,7 +91,7 @@ class W3DHub
|
|||||||
retry_interval: 1,
|
retry_interval: 1,
|
||||||
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
|
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
|
||||||
)
|
)
|
||||||
rescue Excon::Errors::Timeout => e
|
rescue Excon::Error::Timeout => e
|
||||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
||||||
|
|
||||||
DummyResponse.new(e)
|
DummyResponse.new(e)
|
||||||
@@ -129,7 +135,7 @@ class W3DHub
|
|||||||
retry_interval: 1,
|
retry_interval: 1,
|
||||||
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
|
retry_errors: [Excon::Error::Socket, Excon::Error::HTTPStatus] # Don't retry on timeout
|
||||||
)
|
)
|
||||||
rescue Excon::Errors::Timeout => e
|
rescue Excon::Error::Timeout => e
|
||||||
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
|
||||||
|
|
||||||
DummyResponse.new(e)
|
DummyResponse.new(e)
|
||||||
@@ -234,7 +240,7 @@ class W3DHub
|
|||||||
response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend)
|
response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend)
|
||||||
|
|
||||||
if response.status == 200
|
if response.status == 200
|
||||||
Applications.new(response.body)
|
Applications.new(response.body, backend)
|
||||||
else
|
else
|
||||||
logger.error(LOG_TAG) { "Failed to fetch applications list:" }
|
logger.error(LOG_TAG) { "Failed to fetch applications list:" }
|
||||||
logger.error(LOG_TAG) { response }
|
logger.error(LOG_TAG) { response }
|
||||||
@@ -281,14 +287,14 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
# If channel versions and access levels match then all's well
|
# If channel versions and access levels match then all's well
|
||||||
if channel.current_version == _channel.current_version &&
|
if channel.current_version == _channel.current_version &&
|
||||||
channel.user_level == _channel.user_level
|
channel.user_level == _channel.user_level
|
||||||
|
|
||||||
# All's Well!
|
# All's Well!
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
# If the access levels doen't match then overwrite alternate's channel with primary's channel
|
# If the access levels don't match then overwrite alternate's channel with primary's channel
|
||||||
if channel.user_level != _channel.user_level
|
if channel.user_level != _channel.user_level
|
||||||
# Replace alternate's channel with primary's channel
|
# Replace alternate's channel with primary's channel
|
||||||
_game.channels[_game.channels.index(_channel)] = channel
|
_game.channels[_game.channels.index(_channel)] = channel
|
||||||
@@ -296,8 +302,8 @@ class W3DHub
|
|||||||
# Replaced, continue.
|
# Replaced, continue.
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
# If versions doen't match then pick whichever one is higher
|
# If versions don't match then pick whichever one is higher
|
||||||
if Gem::Version.new(channel.current_version) > Gem::Version.new(_channel.current_version)
|
if Gem::Version.new(channel.current_version) > Gem::Version.new(_channel.current_version)
|
||||||
# Replace alternate's channel with primary's channel
|
# Replace alternate's channel with primary's channel
|
||||||
_game.channels[_game.channels.index(_channel)] = channel
|
_game.channels[_game.channels.index(_channel)] = channel
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ class W3DHub
|
|||||||
|
|
||||||
def to_json(env)
|
def to_json(env)
|
||||||
d = @data.dup
|
d = @data.dup
|
||||||
|
|
||||||
d[:avatar_uri] = @avatar_uri
|
d[:avatar_uri] = @avatar_uri
|
||||||
d[:access_token_expiry] = d[:access_token_expiry].to_i
|
d[:access_token_expiry] = @access_token_expiry.to_i
|
||||||
|
|
||||||
d.to_json(env)
|
d.to_json(env)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ class W3DHub
|
|||||||
class Applications
|
class Applications
|
||||||
attr_reader :data
|
attr_reader :data
|
||||||
|
|
||||||
def initialize(response)
|
def initialize(response, source = nil)
|
||||||
@data = JSON.parse(response, symbolize_names: true)
|
@data = JSON.parse(response, symbolize_names: true)
|
||||||
|
|
||||||
games = @data[:applications].select { |a| a[:category] == "games" }
|
games = @data[:applications].select { |a| a[:category] == "games" }
|
||||||
|
|
||||||
@games = []
|
@games = []
|
||||||
|
|
||||||
games.each { |hash| @games << Game.new(hash) }
|
games.each { |hash| @games << Game.new(hash, source) }
|
||||||
@games.sort_by!(&:name).reverse
|
@games.sort_by!(&:name).reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -20,9 +20,11 @@ class W3DHub
|
|||||||
|
|
||||||
class Game
|
class Game
|
||||||
attr_reader :id, :name, :type, :category, :studio_id, :channels, :web_links, :color
|
attr_reader :id, :name, :type, :category, :studio_id, :channels, :web_links, :color
|
||||||
|
attr_reader :___source
|
||||||
|
|
||||||
def initialize(hash)
|
def initialize(hash, source = nil)
|
||||||
@data = hash
|
@data = hash
|
||||||
|
@data[:___source] = source if source
|
||||||
|
|
||||||
@id = @data[:id].to_s
|
@id = @data[:id].to_s
|
||||||
@name = @data[:name]
|
@name = @data[:name]
|
||||||
@@ -31,7 +33,7 @@ class W3DHub
|
|||||||
@studio_id = @data[:"studio-id"]
|
@studio_id = @data[:"studio-id"]
|
||||||
|
|
||||||
# TODO: Do processing
|
# TODO: Do processing
|
||||||
@channels = @data[:channels].map { |channel| Channel.new(channel) }
|
@channels = @data[:channels].map { |channel| Channel.new(channel, source) }
|
||||||
@web_links = @data[:"web-links"]&.map { |link| WebLink.new(link) } || []
|
@web_links = @data[:"web-links"]&.map { |link| WebLink.new(link) } || []
|
||||||
@extended_data = @data[:"extended-data"]
|
@extended_data = @data[:"extended-data"]
|
||||||
|
|
||||||
@@ -55,17 +57,34 @@ class W3DHub
|
|||||||
@uses_ren_folder
|
@uses_ren_folder
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def source
|
||||||
|
@data[:___source]&.to_sym || :w3dhub
|
||||||
|
end
|
||||||
|
|
||||||
|
def source=(sym)
|
||||||
|
@data[:___source] = sym
|
||||||
|
end
|
||||||
|
|
||||||
class Channel
|
class Channel
|
||||||
attr_reader :id, :name, :user_level, :current_version
|
attr_reader :id, :name, :user_level, :current_version
|
||||||
|
|
||||||
def initialize(hash)
|
def initialize(hash, source = nil)
|
||||||
@data = hash
|
@data = hash
|
||||||
|
@data[:___source] = source
|
||||||
|
|
||||||
@id = @data[:id].to_s
|
@id = @data[:id].to_s
|
||||||
@name = @data[:name]
|
@name = @data[:name]
|
||||||
@user_level = @data[:"user-level"]
|
@user_level = @data[:"user-level"]
|
||||||
@current_version = @data[:"current-version"]
|
@current_version = @data[:"current-version"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def source
|
||||||
|
@data[:___source]&.to_sym || :w3dhub
|
||||||
|
end
|
||||||
|
|
||||||
|
def source=(sym)
|
||||||
|
@data[:___source] = sym
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class WebLink
|
class WebLink
|
||||||
|
|||||||
@@ -235,11 +235,11 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def join_server(app_id, channel, server, password = nil)
|
def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false)
|
||||||
if installed?(app_id, channel) && Store.settings[:server_list_username].to_s.length.positive?
|
if installed?(app_id, channel) && username.to_s.length.positive?
|
||||||
run(
|
run(
|
||||||
app_id, channel,
|
app_id, channel,
|
||||||
"+connect #{server.address}:#{server.port} +netplayername #{Store.settings[:server_list_username]}#{password ? " +password \"#{password}\"" : ""}"
|
"+connect #{server.address}:#{server.port} +netplayername #{username}#{password ? " +password \"#{password}\"" : ""}#{multi ? " +multi" : ""}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -112,6 +112,38 @@ class W3DHub
|
|||||||
@task_state == :failed
|
@task_state == :failed
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def normalize_path(path, base_path)
|
||||||
|
path = path.to_s.gsub("\\", "/")
|
||||||
|
return "#{base_path}/#{path}" if W3DHub.windows? # Windows is easy, or annoying, depending how you look at it...
|
||||||
|
|
||||||
|
constructed_path = base_path
|
||||||
|
lowercase_full_path = "#{base_path}/#{path}".downcase.strip.freeze
|
||||||
|
|
||||||
|
accepted_parts = 0
|
||||||
|
split_path = path.split("/")
|
||||||
|
split_path.each do |segment|
|
||||||
|
Dir.glob("#{constructed_path}/*").each do |part|
|
||||||
|
next unless "#{constructed_path}/#{segment}".downcase == part.downcase
|
||||||
|
|
||||||
|
# Handle edge case where a file with the same name is in a higher directory
|
||||||
|
next if File.file?(part) && part.downcase.strip != lowercase_full_path
|
||||||
|
|
||||||
|
constructed_path = part
|
||||||
|
accepted_parts += 1
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Find file if it exists else use provided path as cased
|
||||||
|
if constructed_path.downcase.strip == lowercase_full_path
|
||||||
|
constructed_path
|
||||||
|
elsif accepted_parts.positive?
|
||||||
|
"#{constructed_path}/#{split_path[accepted_parts..].join('/')}"
|
||||||
|
else
|
||||||
|
"#{base_path}/#{path}" # File doesn't exist, case doesn't matter.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def failure_reason
|
def failure_reason
|
||||||
@task_failure_reason || ""
|
@task_failure_reason || ""
|
||||||
end
|
end
|
||||||
@@ -259,7 +291,7 @@ class W3DHub
|
|||||||
next if packages.detect do |pkg|
|
next if packages.detect do |pkg|
|
||||||
pkg.category == "games" &&
|
pkg.category == "games" &&
|
||||||
pkg.subcategory == @app_id &&
|
pkg.subcategory == @app_id &&
|
||||||
pkg.name == file.package &&
|
pkg.name.to_s.casecmp?(file.package.to_s) &&
|
||||||
pkg.version == file.version
|
pkg.version == file.version
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -295,17 +327,13 @@ class W3DHub
|
|||||||
@files.reverse.each do |file|
|
@files.reverse.each do |file|
|
||||||
break unless folder_exists
|
break unless folder_exists
|
||||||
|
|
||||||
safe_file_name = file.name.gsub("\\", "/")
|
file_path = normalize_path(file.name, path)
|
||||||
# Fix borked Data -> data 'cause Windows don't care about capitalization
|
|
||||||
safe_file_name.sub!("Data/", "data/")
|
|
||||||
|
|
||||||
file_path = "#{path}/#{safe_file_name}"
|
|
||||||
|
|
||||||
processed_files += 1
|
processed_files += 1
|
||||||
@status.progress = processed_files.to_f / file_count
|
@status.progress = processed_files.to_f / file_count
|
||||||
|
|
||||||
next if file.removed_since
|
next if file.removed_since
|
||||||
next if accepted_files.key?(safe_file_name)
|
next if accepted_files.key?(file_path)
|
||||||
|
|
||||||
unless File.exist?(file_path)
|
unless File.exist?(file_path)
|
||||||
rejected_files << { file: file, manifest_version: file.version }
|
rejected_files << { file: file, manifest_version: file.version }
|
||||||
@@ -325,7 +353,7 @@ class W3DHub
|
|||||||
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
|
if digest.hexdigest.upcase == file.checksum.upcase
|
||||||
accepted_files[safe_file_name] = file.version
|
accepted_files[file_path] = file.version
|
||||||
logger.info(LOG_TAG) { "[#{file.version}] Verified file: #{file_path}" }
|
logger.info(LOG_TAG) { "[#{file.version}] Verified file: #{file_path}" }
|
||||||
else
|
else
|
||||||
rejected_files << { file: file, manifest_version: file.version }
|
rejected_files << { file: file, manifest_version: file.version }
|
||||||
@@ -369,7 +397,7 @@ class W3DHub
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
package_details = Api.package_details(hashes)
|
package_details = Api.package_details(hashes, @channel.source || :w3dhub)
|
||||||
|
|
||||||
unless package_details
|
unless package_details
|
||||||
fail!("Failed to fetch package details")
|
fail!("Failed to fetch package details")
|
||||||
@@ -383,9 +411,9 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
package = @packages.find do |pkg|
|
package = @packages.find do |pkg|
|
||||||
pkg.category == rich.category &&
|
pkg.category.to_s.casecmp?(rich.category.to_s) &&
|
||||||
pkg.subcategory == rich.subcategory &&
|
pkg.subcategory.to_s.casecmp?(rich.subcategory.to_s) &&
|
||||||
"#{pkg.name}.zip" == rich.name &&
|
"#{pkg.name}.zip".casecmp?(rich.name) &&
|
||||||
pkg.version == rich.version
|
pkg.version == rich.version
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -529,7 +557,7 @@ class W3DHub
|
|||||||
logger.info(LOG_TAG) { " #{file.name}" }
|
logger.info(LOG_TAG) { " #{file.name}" }
|
||||||
|
|
||||||
path = Cache.install_path(@application, @channel)
|
path = Cache.install_path(@application, @channel)
|
||||||
file_path = "#{path}/#{file.name}".sub('Data/', 'data/')
|
file_path = normalize_path(file.name, path)
|
||||||
|
|
||||||
File.delete(file_path) if File.exist?(file_path)
|
File.delete(file_path) if File.exist?(file_path)
|
||||||
|
|
||||||
@@ -562,7 +590,7 @@ class W3DHub
|
|||||||
def write_paths_ini
|
def write_paths_ini
|
||||||
path = Cache.install_path(@application, @channel)
|
path = Cache.install_path(@application, @channel)
|
||||||
|
|
||||||
File.open("#{path}/data/paths.ini", "w") do |file|
|
File.open(normalize_path("data/paths.ini", path), "w") do |file|
|
||||||
file.puts("[paths]")
|
file.puts("[paths]")
|
||||||
file.puts("RegBase=W3D Hub")
|
file.puts("RegBase=W3D Hub")
|
||||||
file.puts("RegClient=#{@application.category}\\#{@application.id}-#{@channel.id}")
|
file.puts("RegClient=#{@application.category}\\#{@application.id}-#{@channel.id}")
|
||||||
@@ -596,7 +624,7 @@ class W3DHub
|
|||||||
# Check for and integrity of local manifest
|
# Check for and integrity of local manifest
|
||||||
|
|
||||||
package = nil
|
package = nil
|
||||||
array = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }])
|
array = Api.package_details([{ category: category, subcategory: subcategory, name: name, version: version }], @channel.source || :w3dhub)
|
||||||
if array.is_a?(Array)
|
if array.is_a?(Array)
|
||||||
package = array.first
|
package = array.first
|
||||||
else
|
else
|
||||||
@@ -707,15 +735,15 @@ class W3DHub
|
|||||||
logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" }
|
logger.info(LOG_TAG) { " Unpacking patch \"#{package_path}\" in \"#{temp_path}\"" }
|
||||||
unzip(package_path, temp_path)
|
unzip(package_path, temp_path)
|
||||||
|
|
||||||
# Fix borked Data -> data 'cause Windows don't care about capitalization
|
file_path = normalize_path(manifest_file.name, path)
|
||||||
safe_file_name = "#{manifest_file.name.sub('Data/', 'data/')}"
|
temp_file_path = normalize_path(manifest_file.name, temp_path)
|
||||||
|
|
||||||
logger.info(LOG_TAG) { " Loading #{temp_path}/#{safe_file_name}.patch..." }
|
logger.info(LOG_TAG) { " Loading #{temp_file_path}.patch..." }
|
||||||
patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_path}/#{safe_file_name}.patch", ignore_crc_mismatches: false)
|
patch_mix = W3DHub::Mixer::Reader.new(file_path: "#{temp_file_path}.patch", ignore_crc_mismatches: false)
|
||||||
patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name.casecmp?(".w3dhub.patch") || f.name.casecmp?(".bhppatch") }.data, symbolize_names: true)
|
patch_info = JSON.parse(patch_mix.package.files.find { |f| f.name.casecmp?(".w3dhub.patch") || f.name.casecmp?(".bhppatch") }.data, symbolize_names: true)
|
||||||
|
|
||||||
logger.info(LOG_TAG) { " Loading #{path}/#{safe_file_name}..." }
|
logger.info(LOG_TAG) { " Loading #{file_path}..." }
|
||||||
target_mix = W3DHub::Mixer::Reader.new(file_path: "#{path}/#{safe_file_name}", ignore_crc_mismatches: false)
|
target_mix = W3DHub::Mixer::Reader.new(file_path: "#{file_path}", ignore_crc_mismatches: false)
|
||||||
|
|
||||||
logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive?
|
logger.info(LOG_TAG) { " Removing files..." } if patch_info[:removedFiles].size.positive?
|
||||||
patch_info[:removedFiles].each do |file|
|
patch_info[:removedFiles].each do |file|
|
||||||
@@ -737,8 +765,8 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.info(LOG_TAG) { " Writing updated #{path}/#{safe_file_name}..." } if patch_info[:updatedFiles].size.positive?
|
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
|
||||||
W3DHub::Mixer::Writer.new(file_path: "#{path}/#{safe_file_name}", package: target_mix.package, memory_buffer: true, encrypted: target_mix.encrypted?)
|
W3DHub::Mixer::Writer.new(file_path: "#{file_path}", package: target_mix.package, memory_buffer: true, encrypted: target_mix.encrypted?)
|
||||||
|
|
||||||
FileUtils.remove_dir(temp_path)
|
FileUtils.remove_dir(temp_path)
|
||||||
|
|
||||||
@@ -749,17 +777,15 @@ class W3DHub
|
|||||||
stream = Zip::InputStream.new(File.open(package_path))
|
stream = Zip::InputStream.new(File.open(package_path))
|
||||||
|
|
||||||
while (entry = stream.get_next_entry)
|
while (entry = stream.get_next_entry)
|
||||||
|
# Normalize the path to handle case-insensitivity consistently
|
||||||
|
file_path = normalize_path(entry.name, path)
|
||||||
|
|
||||||
safe_file_name = entry.name.gsub("\\", "/")
|
dir_path = File.dirname(file_path)
|
||||||
# Fix borked Data -> data 'cause Windows don't care about capitalization
|
|
||||||
safe_file_name.sub!("Data/", "data/")
|
|
||||||
|
|
||||||
dir_path = "#{path}/#{File.dirname(safe_file_name)}"
|
|
||||||
unless dir_path.end_with?("/.") || Dir.exist?(dir_path)
|
unless dir_path.end_with?("/.") || Dir.exist?(dir_path)
|
||||||
FileUtils.mkdir_p(dir_path)
|
FileUtils.mkdir_p(dir_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
File.open("#{path}/#{safe_file_name}", "wb") do |f|
|
File.open(file_path, "wb") do |f|
|
||||||
i = entry.get_input_stream
|
i = entry.get_input_stream
|
||||||
|
|
||||||
while (chunk = i.read(32_000_000)) # Read up to ~32 MB per chunk
|
while (chunk = i.read(32_000_000)) # Read up to ~32 MB per chunk
|
||||||
|
|||||||
@@ -53,4 +53,4 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ class W3DHub
|
|||||||
def self.verify_peer
|
def self.verify_peer
|
||||||
no_verify.tap do |context|
|
no_verify.tap do |context|
|
||||||
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
context.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||||
context.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
|
context.cert_store = OpenSSL::X509::Store.new
|
||||||
|
if (ca_file = W3DHub.ca_bundle_path)
|
||||||
|
context.cert_store.add_file(ca_file)
|
||||||
|
else
|
||||||
|
context.cert_store.set_default_paths
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
22
lib/cache.rb
@@ -90,6 +90,10 @@ class W3DHub
|
|||||||
# Download a W3D Hub package
|
# Download a W3D Hub package
|
||||||
def self.fetch_package(package, block)
|
def self.fetch_package(package, block)
|
||||||
endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package"
|
endpoint_download_url = package.download_url || "#{Api::W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package"
|
||||||
|
if package.download_url
|
||||||
|
uri_path = package.download_url.split("/").last
|
||||||
|
endpoint_download_url = package.download_url.sub(uri_path, URI.encode_uri_component(uri_path))
|
||||||
|
end
|
||||||
path = package_path(package.category, package.subcategory, package.name, package.version)
|
path = package_path(package.category, package.subcategory, package.name, package.version)
|
||||||
headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT }
|
headers = { "Content-Type": "application/x-www-form-urlencoded", "User-Agent": Api::USER_AGENT }
|
||||||
headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account && !package.download_url
|
headers["Authorization"] = "Bearer #{Store.account.access_token}" if Store.account && !package.download_url
|
||||||
@@ -129,10 +133,24 @@ class W3DHub
|
|||||||
return true
|
return 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}" }
|
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
|
||||||
|
|
||||||
false
|
return false
|
||||||
end
|
end
|
||||||
|
rescue Excon::Error::Timeout => e
|
||||||
|
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" timed out after: #{W3DHub::Api::API_TIMEOUT} seconds" }
|
||||||
|
logger.error(LOG_TAG) { e }
|
||||||
|
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
|
||||||
|
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
|
||||||
|
|
||||||
|
return false
|
||||||
|
rescue Excon::Error => e
|
||||||
|
logger.error(LOG_TAG) { " Connection to \"#{endpoint_download_url}\" errored:" }
|
||||||
|
logger.error(LOG_TAG) { e }
|
||||||
|
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" }
|
||||||
|
logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
|
||||||
|
|
||||||
|
return false
|
||||||
ensure
|
ensure
|
||||||
file&.close
|
file&.close
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,6 +32,15 @@ class W3DHub
|
|||||||
linux? || mac?
|
linux? || mac?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Detect system CA bundle path for SSL verification
|
||||||
|
def self.ca_bundle_path
|
||||||
|
[
|
||||||
|
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
|
||||||
|
"/etc/pki/tls/certs/ca-bundle.crt", # RHEL/Fedora/CentOS
|
||||||
|
"/etc/ssl/ca-bundle.pem" # Some other distros
|
||||||
|
].find { |path| File.exist?(path) }
|
||||||
|
end
|
||||||
|
|
||||||
def self.url(path)
|
def self.url(path)
|
||||||
raise "Hazardous input: #{path}" if path.include?("&&") || path.include?(";")
|
raise "Hazardous input: #{path}" if path.include?("&&") || path.include?(";")
|
||||||
|
|
||||||
@@ -72,15 +81,19 @@ class W3DHub
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.join_server(server, password)
|
def self.join_server(server:, username: Store.settings[:server_list_username], password: nil, multi: false)
|
||||||
if (
|
if (
|
||||||
(server.status.password && password.length.positive?) ||
|
(server.status.password && password.length.positive?) ||
|
||||||
!server.status.password) &&
|
!server.status.password) &&
|
||||||
Store.settings[:server_list_username].to_s.length.positive?
|
username.to_s.length.positive?
|
||||||
|
|
||||||
Store.application_manager.join_server(
|
Store.application_manager.join_server(
|
||||||
server.game,
|
server.game,
|
||||||
server.channel, server, password
|
server.channel,
|
||||||
|
server,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
multi
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
|
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
|
||||||
@@ -193,7 +206,7 @@ class W3DHub
|
|||||||
|
|
||||||
path.strip
|
path.strip
|
||||||
else
|
else
|
||||||
result_ptr = LibUI.open_folder(window)
|
result_ptr = LibUI.open_folder(LIBUI_WINDOW)
|
||||||
result = result_ptr.null? ? "" : result_ptr.to_s.gsub("\\", "/")
|
result = result_ptr.null? ? "" : result_ptr.to_s.gsub("\\", "/")
|
||||||
|
|
||||||
result.strip
|
result.strip
|
||||||
|
|||||||
@@ -161,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
|
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
|
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
|
stack(fill: true, height: 1.0, padding: 4, border_thickness_left: 1, border_color_left: lighten(Gosu::Color.new(0xff_252525))) do
|
||||||
tagline "<b>#{item.title}</b>", width: 1.0
|
tagline "<b>#{item.title}</b>", width: 1.0
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class W3DHub
|
|||||||
background app_color
|
background app_color
|
||||||
|
|
||||||
flow(width: 0.70, height: 1.0) do
|
flow(width: 0.70, height: 1.0) do
|
||||||
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{task.app_id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{task.app_id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
image_path = File.exist?("#{CACHE_PATH}/#{task.app_id}.png") ? "#{CACHE_PATH}/#{task.app_id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
||||||
@application_image = image image_path, height: 1.0
|
@application_image = image image_path, height: 1.0
|
||||||
|
|
||||||
stack(margin_left: 8, width: 0.75) do
|
stack(margin_left: 8, width: 0.75) do
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Game Menu
|
# Game Menu
|
||||||
@game_page_container = stack(width: 1.0, fill: true, background_image: "#{GAME_ROOT_PATH}/media/textures/noiseb.png", background_image_mode: :tiled) do
|
@game_page_container = stack(width: 1.0, fill: true) do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -85,7 +85,7 @@ class W3DHub
|
|||||||
padding_left: 4, padding_right: 4, tip: game.name) do
|
padding_left: 4, padding_right: 4, tip: game.name) do
|
||||||
background game.color if selected
|
background game.color if selected
|
||||||
|
|
||||||
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{game.id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
image_path = File.exist?("#{CACHE_PATH}/#{game.id}.png") ? "#{CACHE_PATH}/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
||||||
image_color = Store.application_manager.installed?(game.id, game.channels.first.id) ? 0xff_ffffff : 0x66_ffffff
|
image_color = Store.application_manager.installed?(game.id, game.channels.first.id) ? 0xff_ffffff : 0x66_ffffff
|
||||||
|
|
||||||
flow(width: 1.0, height: 1.0, margin: 8, background_image: image_path, background_image_color: image_color, background_image_mode: :fill_height) do
|
flow(width: 1.0, height: 1.0, margin: 8, background_image: image_path, background_image_color: image_color, background_image_mode: :fill_height) do
|
||||||
@@ -118,28 +118,25 @@ class W3DHub
|
|||||||
|
|
||||||
@game_page_container.clear do
|
@game_page_container.clear do
|
||||||
game_color = Gosu::Color.new(game.color)
|
game_color = Gosu::Color.new(game.color)
|
||||||
game_color.alpha = 0x88
|
game_color.alpha = 0xaa
|
||||||
|
|
||||||
background game_color
|
background_image_path = Cache.package_path(game.category, game.id, "background.png", "")
|
||||||
@game_page_container.style.background_image_color = game_color
|
if File.exist?(background_image_path)
|
||||||
@game_page_container.style.default[:background_image_color] = game_color
|
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path)
|
||||||
@game_page_container.update_background_image
|
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.default[:background_image] = get_image(background_image_path)
|
||||||
|
end
|
||||||
|
|
||||||
# Game Stuff
|
# Game Stuff
|
||||||
flow(width: 1.0, fill: true) do
|
flow(width: 1.0, fill: true) do
|
||||||
# background 0xff_9999ff
|
|
||||||
|
|
||||||
# Game options
|
# Game options
|
||||||
stack(width: 360, height: 1.0, padding: 8, scroll: true, border_thickness_right: 1, border_color_right: W3DHub::BORDER_COLOR) do
|
stack(width: 360, height: 1.0, padding: 8, scroll: true, background: game_color, border_thickness_right: 1, border_color_right: W3DHub::BORDER_COLOR) do
|
||||||
background 0x55_000000
|
# Game Logo
|
||||||
|
logo_image_path = Cache.package_path(game.category, game.id, "logo.png", "")
|
||||||
|
|
||||||
# Game Banner
|
if File.exist?(logo_image_path)
|
||||||
image_path = "#{GAME_ROOT_PATH}/media/banners/#{game.id}.png"
|
image logo_image_path, width: 1.0
|
||||||
|
|
||||||
if File.exist?(image_path)
|
|
||||||
image image_path, width: 1.0
|
|
||||||
else
|
else
|
||||||
banner game.name unless File.exist?(image_path)
|
banner game.name unless File.exist?(logo_image_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do
|
stack(width: 1.0, fill: true, scroll: true, margin_top: 32) do
|
||||||
@@ -331,11 +328,6 @@ class W3DHub
|
|||||||
|
|
||||||
def populate_all_games_view
|
def populate_all_games_view
|
||||||
@game_page_container.clear do
|
@game_page_container.clear do
|
||||||
background 0x88_353535
|
|
||||||
@game_page_container.style.background_image_color = 0x88_353535
|
|
||||||
@game_page_container.style.default[:background_image_color] = 0x88_353535
|
|
||||||
@game_page_container.update_background_image
|
|
||||||
|
|
||||||
@focused_game = nil
|
@focused_game = nil
|
||||||
@focused_channel = nil
|
@focused_channel = nil
|
||||||
|
|
||||||
@@ -383,7 +375,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
container = stack(fill: true, width: 1.0, padding: 8) do
|
container = stack(fill: true, width: 1.0, padding: 8) do
|
||||||
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{game.id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
image_path = File.exist?("#{CACHE_PATH}/#{game.id}.png") ? "#{CACHE_PATH}/#{game.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
||||||
flow(width: 1.0, margin_top: 8) do
|
flow(width: 1.0, margin_top: 8) do
|
||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
image image_path, width: 0.5
|
image image_path, width: 0.5
|
||||||
@@ -438,6 +430,9 @@ class W3DHub
|
|||||||
return unless @focused_game == game
|
return unless @focused_game == game
|
||||||
|
|
||||||
if (feed = @game_news[game.id])
|
if (feed = @game_news[game.id])
|
||||||
|
game_color = Gosu::Color.new(game.color)
|
||||||
|
game_color.alpha = 0xaa
|
||||||
|
|
||||||
@game_news_container.clear do
|
@game_news_container.clear do
|
||||||
# Patch Notes
|
# Patch Notes
|
||||||
if false # Patch notes
|
if false # Patch notes
|
||||||
@@ -467,9 +462,7 @@ class W3DHub
|
|||||||
feed.items.sort_by { |i| i.timestamp }.reverse[0..9].each do |item|
|
feed.items.sort_by { |i| i.timestamp }.reverse[0..9].each do |item|
|
||||||
image_path = Cache.path(item.image)
|
image_path = Cache.path(item.image)
|
||||||
|
|
||||||
flow(width: 1.0, max_width: 869, height: 200, margin: 8, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
|
flow(width: 1.0, max_width: 869, height: 200, margin: 8, background: game_color, border_thickness: 1, border_color: lighten(Gosu::Color.new(game.color))) do
|
||||||
background 0x44_000000
|
|
||||||
|
|
||||||
if File.file?(image_path)
|
if File.file?(image_path)
|
||||||
image image_path, height: 1.0
|
image image_path, height: 1.0
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class W3DHub
|
|||||||
app = Store.applications.games.find { |a| a.id == app_id.to_s }
|
app = Store.applications.games.find { |a| a.id == app_id.to_s }
|
||||||
next unless app
|
next unless app
|
||||||
|
|
||||||
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{app_id}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{app_id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
image_path = File.exist?("#{CACHE_PATH}/#{app.id}.png") ? "#{CACHE_PATH}/#{app.id}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
||||||
|
|
||||||
image image_path, tip: "#{app.name}", height: 1.0,
|
image image_path, tip: "#{app.name}", height: 1.0,
|
||||||
border_thickness_bottom: 1, border_color_bottom: 0x00_000000,
|
border_thickness_bottom: 1, border_color_bottom: 0x00_000000,
|
||||||
@@ -410,11 +410,11 @@ class W3DHub
|
|||||||
if server.status.password
|
if server.status.password
|
||||||
W3DHub.prompt_for_password(
|
W3DHub.prompt_for_password(
|
||||||
accept_callback: proc do |password|
|
accept_callback: proc do |password|
|
||||||
W3DHub.join_server(server, password)
|
W3DHub.join_server(server: server, password: password)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
W3DHub.join_server(server, nil)
|
W3DHub.join_server(server: server)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
@@ -422,18 +422,24 @@ class W3DHub
|
|||||||
if server.status.password
|
if server.status.password
|
||||||
W3DHub.prompt_for_password(
|
W3DHub.prompt_for_password(
|
||||||
accept_callback: proc do |password|
|
accept_callback: proc do |password|
|
||||||
W3DHub.join_server(server, password)
|
W3DHub.join_server(server: server, password: password)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
W3DHub.join_server(server, nil)
|
W3DHub.join_server(server: server)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if W3DHUB_DEVELOPER
|
if W3DHUB_DEVELOPER
|
||||||
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)
|
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
|
button("Multijoin", tip: "Launch multiple clients with configured username_\#{number}", enabled: (game_installed && !game_updatable), **TESTING_BUTTON) do
|
||||||
|
username = Store.settings[:server_list_username]
|
||||||
|
|
||||||
|
client_instances.value.to_i.times do |i|
|
||||||
|
W3DHub.join_server(server: server, username: format("%s_%d", username, i), multi: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
@@ -534,7 +540,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def game_icon(server)
|
def game_icon(server)
|
||||||
image_path = File.exist?("#{GAME_ROOT_PATH}/media/icons/#{server.game.nil? ? 'ren' : server.game}.png") ? "#{GAME_ROOT_PATH}/media/icons/#{server.game.nil? ? 'ren' : server.game}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
image_path = File.exist?("#{CACHE_PATH}/#{server.game.nil? ? 'ren' : server.game}.png") ? "#{CACHE_PATH}/#{server.game.nil? ? 'ren' : server.game}.png" : "#{GAME_ROOT_PATH}/media/icons/default_icon.png"
|
||||||
|
|
||||||
if server.status.password
|
if server.status.password
|
||||||
@server_locked_icons[server.game] ||= Gosu.render(96, 96) do
|
@server_locked_icons[server.game] ||= Gosu.render(96, 96) do
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
stack(width: 128, height: 48, h_align: :center, margin_top: 16) do
|
stack(width: 128, max_height: 256, h_align: :center, margin_top: 16, fill: true) do
|
||||||
button "Save", width: 1.0 do
|
button "Save", width: 1.0 do
|
||||||
save_settings!
|
save_settings!
|
||||||
end
|
end
|
||||||
@@ -84,7 +84,9 @@ class W3DHub
|
|||||||
when "es"
|
when "es"
|
||||||
"Español"
|
"Español"
|
||||||
else
|
else
|
||||||
raise "Unknown language error"
|
logger.warn("W3DHub::Settings") { "Unknown language code: #{string.inspect}" }
|
||||||
|
|
||||||
|
"UNKNOWN"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ class W3DHub
|
|||||||
@fraction = 0.0
|
@fraction = 0.0
|
||||||
@w3dhub_logo = get_image("#{GAME_ROOT_PATH}/media/icons/app.png")
|
@w3dhub_logo = get_image("#{GAME_ROOT_PATH}/media/icons/app.png")
|
||||||
@tasks = {
|
@tasks = {
|
||||||
# connectivity_check: { started: false, complete: false }, # HEAD connectivity-check.ubuntu.com or HEAD secure.w3dhub.com?
|
connectivity_check: { started: false, complete: false }, # HEAD connectivity-check.ubuntu.com or HEAD secure.w3dhub.com?
|
||||||
|
# launcher_updater: { started: false, complete: false },
|
||||||
server_list: { started: false, complete: false },
|
server_list: { started: false, complete: false },
|
||||||
refresh_user_token: { started: false, complete: false },
|
refresh_user_token: { started: false, complete: false },
|
||||||
service_status: { started: false, complete: false },
|
service_status: { started: false, complete: false },
|
||||||
applications: { started: false, complete: false },
|
applications: { started: false, complete: false },
|
||||||
app_icons: { started: false, complete: false }
|
app_icons: { started: false, complete: false },
|
||||||
|
app_logos_and_backgrounds: { started: false, complete: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
@offline_mode = false
|
@offline_mode = false
|
||||||
@@ -137,6 +139,41 @@ class W3DHub
|
|||||||
@tasks[:refresh_user_token][:complete] = true
|
@tasks[:refresh_user_token][:complete] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def connectivity_check
|
||||||
|
domains = {
|
||||||
|
"w3dhub-api.w3d.cyberarm.dev": false,
|
||||||
|
"s3.w3d.cyberarm.dev": false,
|
||||||
|
"secure.w3dhub.com": false
|
||||||
|
}
|
||||||
|
|
||||||
|
@status_label.value = "Checking uplink..."
|
||||||
|
domains.each do |key, value|
|
||||||
|
begin
|
||||||
|
Resolv.getaddress(key.to_s)
|
||||||
|
rescue => e
|
||||||
|
logger.error(LOG_TAG) {"Failed to resolve hostname: #{key.to_s}"}
|
||||||
|
logger.error(LOG_TAG) {e}
|
||||||
|
|
||||||
|
push_state(
|
||||||
|
ConfirmDialog,
|
||||||
|
title: "DNS Resolution Failure",
|
||||||
|
message: "Failed to resolve: #{key.to_s}\n\nTry disabling VPN or proxy if in use.\n\n\nContinue offline?",
|
||||||
|
cancel_callback: ->() { window.close },
|
||||||
|
accept_callback: ->() {
|
||||||
|
@offline_mode = true
|
||||||
|
Store.offline_mode = true
|
||||||
|
@tasks[:connectivity_check][:complete] = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prevent task from being marked as completed
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@tasks[:connectivity_check][:complete] = true
|
||||||
|
end
|
||||||
|
|
||||||
def service_status
|
def service_status
|
||||||
Api.on_thread(:service_status) do |service_status|
|
Api.on_thread(:service_status) do |service_status|
|
||||||
@service_status = service_status
|
@service_status = service_status
|
||||||
@@ -159,6 +196,32 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def launcher_updater
|
||||||
|
@status_label.value = "Checking for Launcher updates..." # I18n.t(:"boot.checking_for_updates")
|
||||||
|
|
||||||
|
Api.on_thread(:fetch, "https://api.github.com/repos/Inq8/CAmod/releases/latest") do |response|
|
||||||
|
if response.status == 200
|
||||||
|
hash = JSON.parse(response.body, symbolize_names: true)
|
||||||
|
available_version = hash[:tag_name].downcase.sub("v", "")
|
||||||
|
|
||||||
|
pp Gem::Version.new(available_version) > Gem::Version.new(W3DHub::VERSION)
|
||||||
|
pp [Gem::Version.new(available_version), Gem::Version.new(W3DHub::VERSION)]
|
||||||
|
|
||||||
|
push_state(
|
||||||
|
LauncherUpdaterDialog,
|
||||||
|
release_data: hash,
|
||||||
|
available_version: available_version,
|
||||||
|
cancel_callback: -> { @tasks[:launcher_updater][:complete] = true },
|
||||||
|
accept_callback: -> { @tasks[:launcher_updater][:complete] = true }
|
||||||
|
)
|
||||||
|
else
|
||||||
|
# Failed to retrieve release data from github
|
||||||
|
log "Failed to retrieve release data from Github"
|
||||||
|
@tasks[:launcher_updater][:complete] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def applications
|
def applications
|
||||||
@status_label.value = I18n.t(:"boot.checking_for_updates")
|
@status_label.value = I18n.t(:"boot.checking_for_updates")
|
||||||
|
|
||||||
@@ -180,6 +243,8 @@ class W3DHub
|
|||||||
def app_icons
|
def app_icons
|
||||||
return unless Store.applications
|
return unless Store.applications
|
||||||
|
|
||||||
|
@status_label.value = "Retrieving application icons, this might take a moment..." # I18n.t(:"boot.checking_for_updates")
|
||||||
|
|
||||||
packages = []
|
packages = []
|
||||||
Store.applications.games.each do |app|
|
Store.applications.games.each do |app|
|
||||||
packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" }
|
packages << { category: app.category, subcategory: app.id, name: "#{app.id}.ico", version: "" }
|
||||||
@@ -192,7 +257,7 @@ class W3DHub
|
|||||||
next if package.error?
|
next if package.error?
|
||||||
|
|
||||||
path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
||||||
generated_icon_path = "#{GAME_ROOT_PATH}/media/icons/#{package.subcategory}.png"
|
generated_icon_path = "#{CACHE_PATH}/#{package.subcategory}.png"
|
||||||
|
|
||||||
regenerate = false
|
regenerate = false
|
||||||
|
|
||||||
@@ -214,6 +279,34 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def app_logos_and_backgrounds
|
||||||
|
return unless Store.applications
|
||||||
|
|
||||||
|
@status_label.value = "Retrieving application image assets, this might take a moment..." # I18n.t(:"boot.checking_for_updates")
|
||||||
|
|
||||||
|
packages = []
|
||||||
|
Store.applications.games.each do |app|
|
||||||
|
packages << { category: app.category, subcategory: app.id, name: "logo.png", version: "" }
|
||||||
|
packages << { category: app.category, subcategory: app.id, name: "background.png", version: "" }
|
||||||
|
end
|
||||||
|
|
||||||
|
Api.on_thread(:package_details, packages, :alt_w3dhub) do |package_details|
|
||||||
|
package_details ||= nil
|
||||||
|
|
||||||
|
package_details&.each do |package|
|
||||||
|
next if package.error?
|
||||||
|
|
||||||
|
package_cache_path = Cache.package_path(package.category, package.subcategory, package.name, package.version)
|
||||||
|
|
||||||
|
missing_or_broken_image = File.exist?(package_cache_path) ? Digest::SHA256.new.hexdigest(File.binread(package_cache_path)).upcase != package.checksum.upcase : true
|
||||||
|
|
||||||
|
Cache.fetch_package(package, proc {}) if missing_or_broken_image
|
||||||
|
end
|
||||||
|
|
||||||
|
@tasks[:app_logos_and_backgrounds][:complete] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def server_list
|
def server_list
|
||||||
@status_label.value = I18n.t(:"server_browser.fetching_server_list")
|
@status_label.value = I18n.t(:"server_browser.fetching_server_list")
|
||||||
|
|
||||||
|
|||||||
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
|
stack(width: 1.0, height: 46, padding: 8) do
|
||||||
button "Okay", width: 1.0 do
|
button "Okay", width: 1.0 do
|
||||||
pop_state
|
pop_state
|
||||||
|
@options[:accept_callback]&.call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class W3DHub
|
|||||||
APPLICATIONS_UPDATE_INTERVAL = 10 * 60 * 1000 # ten minutes
|
APPLICATIONS_UPDATE_INTERVAL = 10 * 60 * 1000 # ten minutes
|
||||||
SERVER_LIST_UPDATE_INTERVAL = 5 * 60 * 1000 # five minutes
|
SERVER_LIST_UPDATE_INTERVAL = 5 * 60 * 1000 # five minutes
|
||||||
|
|
||||||
|
DEFAULT_BACKGROUND_IMAGE = "#{GAME_ROOT_PATH}/media/banners/background.png".freeze
|
||||||
|
|
||||||
attr_accessor :interface_task_update_pending
|
attr_accessor :interface_task_update_pending
|
||||||
|
|
||||||
@@instance = nil
|
@@instance = nil
|
||||||
@@ -33,10 +35,12 @@ class W3DHub
|
|||||||
|
|
||||||
theme(W3DHub::THEME)
|
theme(W3DHub::THEME)
|
||||||
|
|
||||||
@interface_container = stack(width: 1.0, height: 1.0, border_thickness: 1, border_color: W3DHub::BORDER_COLOR, background_image: "#{GAME_ROOT_PATH}/media/banners/background.png", background_image_color: 0xff_525252, background_image_mode: :fill) do
|
@interface_container = stack(width: 1.0, height: 1.0, border_thickness: 1, border_color: W3DHub::BORDER_COLOR, background_image: DEFAULT_BACKGROUND_IMAGE, background_image_mode: :fill) do
|
||||||
background 0xff_252525
|
background 0xff_252525
|
||||||
|
|
||||||
@header_container = flow(width: 1.0, height: 84, padding: 4, border_thickness_bottom: 1, border_color_bottom: W3DHub::BORDER_COLOR) do
|
@header_container = flow(width: 1.0, height: 84, padding: 4, border_thickness_bottom: 1, border_color_bottom: W3DHub::BORDER_COLOR) do
|
||||||
|
background 0xaa_151515
|
||||||
|
|
||||||
flow(width: 148, height: 1.0) do
|
flow(width: 148, height: 1.0) do
|
||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 84
|
image "#{GAME_ROOT_PATH}/media/icons/app.png", height: 84
|
||||||
@@ -54,18 +58,26 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::ServerBrowser)
|
page(W3DHub::Pages::ServerBrowser)
|
||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::Community)
|
page(W3DHub::Pages::Community)
|
||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::DownloadManager)
|
page(W3DHub::Pages::DownloadManager)
|
||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::Settings)
|
page(W3DHub::Pages::Settings)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class W3DHub
|
class W3DHub
|
||||||
DIR_NAME = "W3DHubAlt"
|
DIR_NAME = "W3DHubAlt".freeze
|
||||||
VERSION = "0.7.0"
|
VERSION = "0.8.1".freeze
|
||||||
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 |
@@ -20,12 +20,19 @@ class W3DHub
|
|||||||
W3DHUB_DEBUG = ARGV.join.include?("--debug")
|
W3DHUB_DEBUG = ARGV.join.include?("--debug")
|
||||||
W3DHUB_DEVELOPER = ARGV.join.include?("--developer")
|
W3DHUB_DEVELOPER = ARGV.join.include?("--developer")
|
||||||
|
|
||||||
GAME_ROOT_PATH = File.expand_path(".", __dir__)
|
# Use the real working directory as the root for runtime data/logs
|
||||||
|
GAME_ROOT_PATH = Dir.pwd
|
||||||
|
|
||||||
CACHE_PATH = "#{GAME_ROOT_PATH}/data/cache"
|
CACHE_PATH = "#{GAME_ROOT_PATH}/data/cache"
|
||||||
|
LOGS_PATH = "#{GAME_ROOT_PATH}/data/logs"
|
||||||
SETTINGS_FILE_PATH = "#{GAME_ROOT_PATH}/data/settings.json"
|
SETTINGS_FILE_PATH = "#{GAME_ROOT_PATH}/data/settings.json"
|
||||||
APPLICATIONS_CACHE_FILE_PATH = "#{GAME_ROOT_PATH}/data/applications_cache.json"
|
APPLICATIONS_CACHE_FILE_PATH = "#{GAME_ROOT_PATH}/data/applications_cache.json"
|
||||||
|
|
||||||
LOGGER = Logger.new("#{GAME_ROOT_PATH}/data/logs/w3d_hub_linux_launcher.log", "daily")
|
# Ensure data/cache and data/logs exist
|
||||||
|
FileUtils.mkdir_p(CACHE_PATH) unless Dir.exist?(CACHE_PATH)
|
||||||
|
FileUtils.mkdir_p(LOGS_PATH) unless Dir.exist?(LOGS_PATH)
|
||||||
|
|
||||||
|
LOGGER = Logger.new("#{LOGS_PATH}/w3d_hub_linux_launcher.log", "daily")
|
||||||
LOGGER.level = Logger::Severity::DEBUG # W3DHUB_DEBUG ? Logger::Severity::DEBUG : Logger::Severity::WARN
|
LOGGER.level = Logger::Severity::DEBUG # W3DHUB_DEBUG ? Logger::Severity::DEBUG : Logger::Severity::WARN
|
||||||
|
|
||||||
LOG_TAG = "W3DHubLinuxLauncher"
|
LOG_TAG = "W3DHubLinuxLauncher"
|
||||||
@@ -121,6 +128,7 @@ require_relative "lib/states/dialogs/confirm_dialog"
|
|||||||
require_relative "lib/states/dialogs/direct_connect_dialog"
|
require_relative "lib/states/dialogs/direct_connect_dialog"
|
||||||
require_relative "lib/states/dialogs/game_settings_dialog"
|
require_relative "lib/states/dialogs/game_settings_dialog"
|
||||||
require_relative "lib/states/dialogs/import_game_dialog"
|
require_relative "lib/states/dialogs/import_game_dialog"
|
||||||
|
require_relative "lib/states/dialogs/launcher_updater_dialog"
|
||||||
|
|
||||||
require_relative "lib/api"
|
require_relative "lib/api"
|
||||||
require_relative "lib/api/service_status"
|
require_relative "lib/api/service_status"
|
||||||
|
|||||||