11 Commits

15 changed files with 216 additions and 63 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:

13
Gemfile
View File

@@ -6,7 +6,6 @@ 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"
@@ -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

@@ -3,17 +3,17 @@ GEM
specs: specs:
base64 (0.3.0) base64 (0.3.0)
concurrent-ruby (1.3.5) concurrent-ruby (1.3.5)
cri (2.15.12)
cyberarm_engine (0.24.5) cyberarm_engine (0.24.5)
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) event_emitter (0.2.6)
excon (1.3.0) excon (1.3.2)
logger logger
ffi (1.17.2-x64-mingw-ucrt) ffi (1.17.0)
ffi (1.17.2-x86_64-linux-gnu) ffi-win32-extensions (1.1.0)
ffi-win32-extensions (1.0.4) ffi (>= 1.15.5, <= 1.17.0)
ffi
fiddle (1.1.8) fiddle (1.1.8)
gosu (1.4.6) gosu (1.4.6)
i18n (1.14.7) i18n (1.14.7)
@@ -23,9 +23,16 @@ GEM
fiddle fiddle
logger (1.7.0) logger (1.7.0)
mutex_m (0.3.0) mutex_m (0.3.0)
rake (13.3.0) ocran (1.3.17)
fiddle (~> 1.0)
rake (13.3.1)
releasy (0.2.4)
bundler (>= 1.2.1)
cri (~> 2.15.0)
ocran (~> 1.3.0)
rake (>= 0.9.2.2)
rexml (3.4.4) 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) websocket (1.2.11)
@@ -42,16 +49,19 @@ GEM
PLATFORMS PLATFORMS
x64-mingw-ucrt x64-mingw-ucrt
x86_64-linux
DEPENDENCIES DEPENDENCIES
base64 base64
bundler (~> 2.4.3)
cyberarm_engine cyberarm_engine
digest-crc digest-crc
excon excon
i18n i18n
ircparser ircparser
libui libui
ocran
rake
releasy
rexml rexml
rubyzip rubyzip
sdl2-bindings sdl2-bindings
@@ -60,4 +70,4 @@ DEPENDENCIES
win32-security win32-security
BUNDLED WITH BUNDLED WITH
2.6.8 2.4.22

View File

@@ -3,7 +3,7 @@ It runs natively on Linux! No mucking about trying to get .NET 4.6.1 or somethin
Only requires OpenGL, Ruby, and a few gems. Only requires OpenGL, Ruby, and a few gems.
## Installing ## Installing
* Install Ruby 3.0+, from your package manager. * Install Ruby 3.4+, from your package manager.
* Install Gosu's [dependencies](https://github.com/gosu/gosu/wiki/Getting-Started-on-Linux). * Install Gosu's [dependencies](https://github.com/gosu/gosu/wiki/Getting-Started-on-Linux).
* Install required gems: `bundle install` * Install required gems: `bundle install`

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

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

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

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

View File

@@ -333,7 +333,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 +390,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[: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,16 @@ 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)}", **DANGEROUS_BUTTON) do
# TODO.
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,28 +149,26 @@ 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 StandardError => e
rescue => e logger.error(LOG_TAG) { "Failed to resolve hostname: #{key}" }
logger.error(LOG_TAG) {"Failed to resolve hostname: #{key.to_s}"} 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
} }
) )
# 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
@@ -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", "")
@@ -232,7 +234,7 @@ class W3DHub
@tasks[:applications][:complete] = true @tasks[:applications][:complete] = true
else else
# FIXME: Failed to retreive! # FIXME: Failed to retreive!
BackgroundWorker.foreground_job(-> {}, ->(_){ @status_label.value = "FAILED TO RETREIVE APPS LIST" }) BackgroundWorker.foreground_job(-> {}, ->(_) { @status_label.value = "FAILED TO RETREIVE APPS LIST" })
@offline_mode = true @offline_mode = true
Store.offline_mode = true Store.offline_mode = true
@@ -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
Cache.fetch_package(package, proc {}) begin
regenerate = true Cache.fetch_package(package, proc {})
regenerate = true
rescue Errno::EACCES => e
failure = true
push_state(MessageDialog, title: "Fatal Error",
message: "Directory Permission Error (#{e.class}):\n#{e}.\n\nIs the required drive mounted?",
accept_callback: -> { window.close })
end
end end
if regenerate next unless regenerate
BackgroundWorker.foreground_job(-> { ICO.new(file: path) }, ->(result) { result.save(result.images.max_by(&:width), generated_icon_path) })
end BackgroundWorker.foreground_job(-> { ICO.new(file: path) }, lambda { |result|
result.save(result.images.max_by(&:width), generated_icon_path)
})
end end
@tasks[:app_icons][:complete] = true @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

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

View File

@@ -84,11 +84,11 @@ class W3DHub
BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff") BLACK_IMAGE = Gosu::Image.from_blob(1, 1, "\x00\x00\x00\xff")
end end
require "i18n"
require "websocket-client-simple" require "websocket-client-simple"
require "English" require "English"
require "sdl2" require "sdl2"
require_relative "lib/i18n"
I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"] I18n.load_path << Dir["#{W3DHub::GAME_ROOT_PATH}/locales/*.yml"]
I18n.default_locale = :en I18n.default_locale = :en
@@ -104,7 +104,7 @@ require_relative "lib/cache"
require_relative "lib/settings" require_relative "lib/settings"
require_relative "lib/mixer" require_relative "lib/mixer"
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/background_worker" require_relative "lib/background_worker"