Added GSH game server data containing class, stub Account and Join Server dialogs, support 'lazy loading' remote images (downloading images in the background then calling a callback to replace temp image if host element is still in layout), misc. other changes.

This commit is contained in:
2026-09-02 11:17:04 -05:00
parent 59d884aad1
commit baec1e1e18
12 changed files with 248 additions and 25 deletions

25
lib/dialogs/account.rb Normal file
View File

@@ -0,0 +1,25 @@
module W3DHubLauncher
class Dialog
class Account < W3DHubLauncher::Dialog
def setup
super
stack(width: 1.0, max_width: 600, height: 1.0, max_height: 600, h_align: :center, v_align: :center, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
flow(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
banner "Account", width: 1.0, text_align: :center
end
stack(width: 1.0, fill: true, scroll: true, padding_left: PADDING, padding_right: PADDING) do
button "Sign In", tip: "https://sso.w3d.cyberarm.dev" do
# use SSO service
pop_state
end
end
flow(width: 1.0, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
end
end
end
end
end
end

38
lib/dialogs/server.rb Normal file
View File

@@ -0,0 +1,38 @@
module W3DHubLauncher
class Dialog
class Server < W3DHubLauncher::Dialog
def setup
super
stack(width: 1.0, max_width: 600, height: 1.0, max_height: 600, h_align: :center, v_align: :center, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
flow(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
banner "Downloads", width: 1.0, text_align: :center
end
stack(width: 1.0, fill: true, scroll: true, padding_left: PADDING, padding_right: PADDING) do
50.times do
stack(width: 1.0, margin_top: HALF_PADDING, margin_bottom: HALF_PADDING) do
flow(width: 1.0) do
para "PACKAGE_NAME.mix"
flow(fill: true)
para ["Downloading...", "Pending...", "Done.", "Patching...", "Unpacking..."].sample
end
progress(width: 1.0, fraction: rand)
end
end
end
flow(width: 1.0, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
flow(fill: true)
button "Close" do
pop_state
end
flow(fill: true)
end
end
end
end
end
end

View File

@@ -6,7 +6,11 @@ module W3DHubLauncher
def safe_get_image(path, retro: false) def safe_get_image(path, retro: false)
raise RuntimeError, "Images may only be loaded from the main thread!" unless Thread.current == Thread.main raise RuntimeError, "Images may only be loaded from the main thread!" unless Thread.current == Thread.main
return get_image(path, retro: retro) if File.exist?(path) begin
return get_image(path, retro: retro) if File.exist?(path)
rescue RuntimeError => e
pp e
end
path = "#{ROOT_PATH}/media/default.png" path = "#{ROOT_PATH}/media/default.png"
return get_image(path, retro: retro) if File.exist?(path) return get_image(path, retro: retro) if File.exist?(path)
@@ -24,6 +28,30 @@ module W3DHubLauncher
end end
end end
# Downloads requested url and on success check if host element is still in the layout and call the block if so.
def remote_image(path, url:, element:, &block)
memcache_key = :"lock_net_download_#{element.style.tag}"
if !File.exist?(path) && MemCache[memcache_key].nil?
MemCache[memcache_key] = true
Worker::Api.download_url(url, path) do |result|
# ignore progress reports
next unless result.okay? && result.data == true
MemCache.delete(memcache_key)
# does the target element still exist in the layout?
e = find_element_by_tag(element.root, element.style.tag)
next unless e
if result.okay?
block.call(element, path)
end
end
end
end
def on_main_thread(block) def on_main_thread(block)
window.add_to_queue(block) window.add_to_queue(block)
end end

View File

@@ -57,11 +57,11 @@ module W3DHubLauncher
after(3000) do after(3000) do
puts "HELLO" puts "HELLO"
W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_applications }) do |result| W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_applications }) do |result|
pp [:CALLBACK, result] # pp [:CALLBACK, result]
File.write("applications.json", result.data) File.write("applications.json", result.data)
hash = JSON.parse(result.data) hash = JSON.parse(result.data)
pp applications = hash["applications"]&.map { |app| W3DHubLauncher::Worker::Api::Application.new(app) } || [] applications = hash["applications"]&.map { |app| W3DHubLauncher::Worker::Api::Application.new(app) } || []
pp applications.to_json # pp applications.to_json
MemCache[:applications] = applications MemCache[:applications] = applications
end end

View File

@@ -39,6 +39,13 @@ module W3DHubLauncher
end end
end end
populate_game
end
def populate_game(game = @current_app, channel = @current_channel)
@current_app = game
@current_channel = game&.channels&.first
populate_games_list populate_games_list
populate_game_info populate_game_info
populate_game_event populate_game_event
@@ -49,24 +56,12 @@ module W3DHubLauncher
@games_list_container.clear do @games_list_container.clear do
@games.each_with_index do |game, i| @games.each_with_index do |game, i|
if i.zero? if i.zero?
image(safe_get_image("#{ROOT_PATH}/data/cache/#{game.id}.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0x88_5e5c64, border_thickness_bottom: 3, border_color_bottom: 0xff_3584e4, tip: game.name)do image(safe_get_image("#{ROOT_PATH}/data/cache/#{game.id}.png"), tag: :"image_icon_#{game.id}", height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0x88_5e5c64, border_thickness_bottom: 3, border_color_bottom: 0xff_3584e4, tip: game.name)do
@current_app = game populate_game(game, game&.channels&.first)
@current_channel = game&.channels&.first
populate_games_list
populate_game_info
populate_game_event
populate_game_news
end end
else else
image(safe_get_image("#{ROOT_PATH}/data/cache/#{game.id}.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: game.name)do image(safe_get_image("#{ROOT_PATH}/data/cache/#{game.id}.png"), tag: :"image_icon_#{game.id}", height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: game.name)do
@current_app = game populate_game(game, game&.channels&.first)
@current_channel = game&.channels&.first
populate_games_list
populate_game_info
populate_game_event
populate_game_news
end end
end end
end end
@@ -76,7 +71,10 @@ module W3DHubLauncher
def populate_game_info def populate_game_info
@game_info_container.clear do @game_info_container.clear do
# logo # logo
image safe_get_image("#{ROOT_PATH}/media/#{@current_app.id}.png"), width: 1.0, max_height: 124 img = image safe_get_image("#{ROOT_PATH}/data/cache/logo_#{@current_app.id}.png"), tag: :"image_logo_#{@current_app.id}", width: 1.0, max_height: 124
remote_image("#{ROOT_PATH}/data/cache/logo_#{@current_app.id}.png", url: "https://s3.w3d.cyberarm.dev/games/#{@current_app.id}/logo.png", element: img) do |e, path|
e&.value = safe_get_image(path)
end
# web links # web links
stack(width: 1.0, fill: true, padding: 0, padding_top: LARGE_PADDING) do stack(width: 1.0, fill: true, padding: 0, padding_top: LARGE_PADDING) do

View File

@@ -53,7 +53,10 @@ module W3DHubLauncher
# self account container # self account container
flow(width: 300, height: 80, margin_left: LARGE_PADDING) do flow(width: 300, height: 80, margin_left: LARGE_PADDING) do
# self avatar container # self avatar container
stack(width: 80, height: 1.0, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png"))) do stack(width: 80, height: 1.0, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png"))) do |i|
i.subscribe(:clicked_left_mouse_button) do
dialog(Dialog::Account)
end
# self online state container # self online state container
stack(width: 20, height: 20, v_align: :bottom, h_align: :right, background_image: safe_get_image("#{ROOT_PATH}/media/ui/circle_small.png"), background_image_color: 0xff_26a269) stack(width: 20, height: 20, v_align: :bottom, h_align: :right, background_image: safe_get_image("#{ROOT_PATH}/media/ui/circle_small.png"), background_image_color: 0xff_26a269)
end end

View File

@@ -1,5 +1,10 @@
module W3DHubLauncher module W3DHubLauncher
class Worker class Worker
DEFAULT_HEADERS = [
["user-agent", W3DHubLauncher::USER_AGENT]
].freeze
DEFAULT_NETWORK_TIMEOUT = 30
Response = Data.define(:status, :request_id, :data) Response = Data.define(:status, :request_id, :data)
def initialize def initialize
@@ -109,6 +114,49 @@ module W3DHubLauncher
end end
def download_url(query) def download_url(query)
result = CyberarmEngine::Result.new
method = query.data["method"]
url = query.data["url"]
path = query.data["path"]
headers = query.data["headers"] || DEFAULT_HEADERS
body = query.data["body"]
Sync do |task|
task.with_timeout(DEFAULT_NETWORK_TIMEOUT) do
Async::HTTP::Internet.send(method, url, headers, body) do |response|
if response.success?
content_length = response.headers["content-length"] || 0
total_downloaded_bytes = 0
File.open(path, "wb") do |file|
response.each do |chunk|
file.write(chunk)
downloaded_bytes = chunk.length
total_downloaded_bytes += downloaded_bytes
progress_result = CyberarmEngine::Result.new(data: {
downloaded_bytes: downloaded_bytes,
total_downloaded_bytes: total_downloaded_bytes,
content_length: content_length
})
# FIXME: Send intermediate response to requester
# send(query, Response.new(Request::STATUS_IN_PROGRESS, query.request_id progress_result))
end
end
result.data = true
end
# rescue StandardError => e
# result.error = e
end
# rescue Async::TimeoutError
# result.error = e
end
end
deliver_response(result, query)
end end
def w3dhub_api_call(query) def w3dhub_api_call(query)

View File

@@ -8,7 +8,8 @@ module W3DHubLauncher
end end
# downloads requested resource, periodically reporting progress until completion, returning path for file on disk # downloads requested resource, periodically reporting progress until completion, returning path for file on disk
def self.download_url def self.download_url(url, path, method = :get, headers = nil, body = nil, &block)
Worker::Request.new(:download_url, { url: url, path: path, method: method, headers: headers, body: body }, &block)
end end
# returns user account data # returns user account data

View File

@@ -5,7 +5,7 @@ module W3DHubLauncher
attr_reader :id, :category, :name, :type, :channels, :extended_data, :web_links attr_reader :id, :category, :name, :type, :channels, :extended_data, :web_links
def initialize(data) def initialize(data)
pp data # pp data
@id = data["id"] @id = data["id"]
@category = data["category"] @category = data["category"]

View File

@@ -0,0 +1,79 @@
module W3DHubLauncher
class Worker
class Api
# Designed to work with cyberarm's Game Server Hub service data, may work with W3D Hub's service with degraded metadata.
class GameServer
attr_reader :id, :game, :channel, :address, :port, :region, :version
def initialize(data)
# main data
@id = data["id"]
@game = data["game"]
@channel = data["channel"]
@address = data["address"]
@port = data["port"]
@region = data["region"]
# status data
@name = data["status"]["name"]
@password = data["status"]["password"] || false
@current_map = data["status"]["map"]
@player_count = data["status"]["numplayers"] || 0
@max_players = data["status"]["maxplayers"]
@match_start_time = data["status"]["started"]
@estimated_end_time = data["status"]["estimatedEndTime"]
@match_remaining_time = data["status"]["remaining"]
# status teams data
@teams = []
data["status"]["teams"].each do |team_data|
@teams << Team.new(team_data)
end
# status player data
data["status"]["players"].each do |player_data|
team = @teams.find { |t| t.id == player_data["id"] }
next unless team
team.players << Player.new(player_data)
end
# Cyberarm GSH extras
@version = data["version"] || "838"
@next_map = data["status"]["nextmap"] || ""
end
end
class Team
attr_reader :id, :name, :score, :kills, :deaths, :players
def initialize(data)
@id = data["id"]
@name = data["name"]
@score = data["score"]
@kills = data["kills"]
@deaths = data["deaths"]
@players = []
end
end
class Player
attr_reader :name, :team, :score, :kills, :deaths, :ping, :time
def initialize(data)
@name = data["nick"]
@team = data["team"]
@score = data["score"]
@kills = data["kills"]
@deaths = data["deaths"]
# Cyberarm GSH extras
@ping = data["ping"] || 0
@time = data["time"] || "00:00:00"
end
end
end
end
end

View File

@@ -118,7 +118,7 @@ module W3DHubLauncher
end end
# We've gotten data back from both backends, merge them. # We've gotten data back from both backends, merge them.
pp [primary_result, alternate_result] # pp [primary_result, alternate_result]
# FIXME: Merge primary and alternate results # FIXME: Merge primary and alternate results
result result

View File

@@ -30,6 +30,8 @@ require_relative "lib/pages/boot/initial_setup"
require_relative "lib/pages/boot/start_up" require_relative "lib/pages/boot/start_up"
require_relative "lib/dialogs/about" require_relative "lib/dialogs/about"
require_relative "lib/dialogs/downloads" require_relative "lib/dialogs/downloads"
require_relative "lib/dialogs/server"
require_relative "lib/dialogs/account"
require_relative "lib/states/boot" require_relative "lib/states/boot"
require_relative "lib/states/interface" require_relative "lib/states/interface"
require_relative "lib/window" require_relative "lib/window"
@@ -37,6 +39,7 @@ require_relative "lib/window"
require_relative "lib/worker" require_relative "lib/worker"
require_relative "lib/worker/api" require_relative "lib/worker/api"
require_relative "lib/worker/api/application" require_relative "lib/worker/api/application"
require_relative "lib/worker/api/game_server"
require_relative "lib/worker/request" require_relative "lib/worker/request"
require_relative "lib/worker/w3dhub_api" require_relative "lib/worker/w3dhub_api"
require_relative "lib/worker/task" require_relative "lib/worker/task"