mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-09-20 15:23:53 +00:00
Compare commits
26 Commits
915e643a98
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 53e7a72ceb | |||
| 0a1909a84c | |||
| b7739de136 | |||
| 17614f4416 | |||
| f8ceb69402 | |||
| 6707f260c9 | |||
| 56699bb644 | |||
| 09ef9d613f | |||
| b505f22b8c | |||
| b551312751 | |||
| 1d3164da72 | |||
| 73a9a80a34 | |||
| 7adb23d71f | |||
| 4ba71b760a | |||
| db0dd9fc7c | |||
| 2cf8146553 | |||
| af438e35bc | |||
| 2f07cde109 | |||
| 5092c828a7 | |||
| 21e0888f8e | |||
| 132d39d3a4 | |||
| 7a5667a53a | |||
| 79dfa963c2 | |||
| b0fefb9f7f | |||
| 0693733179 | |||
| b52f6e2183 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
pkg/
|
||||
*.json
|
||||
*.xml
|
||||
data/cache/*
|
||||
!data/cache/.gitkeep
|
||||
data/logs/*
|
||||
|
||||
31
Gemfile.lock
31
Gemfile.lock
@@ -1,16 +1,16 @@
|
||||
GEM
|
||||
remote: https://gem.coop/
|
||||
specs:
|
||||
async (2.45.1)
|
||||
async (2.46.0)
|
||||
console (~> 1.29)
|
||||
fiber-annotation
|
||||
io-event (~> 1.21)
|
||||
async-http (0.103.0)
|
||||
async-http (0.105.0)
|
||||
async (>= 2.35.1)
|
||||
async-pool (~> 0.12)
|
||||
io-endpoint (~> 0.18)
|
||||
io-stream (~> 0.14)
|
||||
protocol-http (~> 0.66)
|
||||
protocol-http (~> 0.72)
|
||||
protocol-http1 (~> 0.41)
|
||||
protocol-http2 (~> 0.28)
|
||||
protocol-url (~> 0.2)
|
||||
@@ -23,7 +23,7 @@ GEM
|
||||
protocol-websocket (~> 0.17)
|
||||
base64 (0.3.0)
|
||||
chunky_png (1.4.0)
|
||||
console (1.37.0)
|
||||
console (1.38.0)
|
||||
fiber-annotation
|
||||
fiber-local (~> 1.1)
|
||||
json
|
||||
@@ -40,21 +40,22 @@ GEM
|
||||
ffi (~> 1.12)
|
||||
io-endpoint (0.18.0)
|
||||
openssl (>= 3.3.0)
|
||||
io-event (1.21.1)
|
||||
io-event (1.22.0)
|
||||
io-stream (0.14.0)
|
||||
openssl (>= 3.3)
|
||||
ircparser (1.0.0)
|
||||
json (3.0.1)
|
||||
json (3.0.2)
|
||||
logger (1.7.0)
|
||||
openssl (4.0.2)
|
||||
protocol-hpack (1.5.1)
|
||||
protocol-http (0.71.0)
|
||||
protocol-http (0.72.0)
|
||||
protocol-http1 (0.41.0)
|
||||
protocol-http (~> 0.68)
|
||||
protocol-http2 (0.28.0)
|
||||
protocol-hpack (~> 1.4)
|
||||
protocol-http (~> 0.62)
|
||||
protocol-rack (0.22.1)
|
||||
protocol-rack (0.23.0)
|
||||
console (~> 1.0)
|
||||
io-stream (>= 0.10)
|
||||
protocol-http (~> 0.58)
|
||||
rack (>= 1.0)
|
||||
@@ -88,13 +89,13 @@ DEPENDENCIES
|
||||
sdl3-bindings
|
||||
|
||||
CHECKSUMS
|
||||
async (2.45.1) sha256=95849f7432eca535146c69f3078dab3c7228d0075ed71f48733f3e585dd7c267
|
||||
async-http (0.103.0) sha256=137e3d5d419f7d72d5fa33cf04f30232e79c57b690025a650b8f9e48ece8bc41
|
||||
async (2.46.0) sha256=34f984733dbf63a997376346451ec995e858cacc3105e86ccfeaf8877b4f490b
|
||||
async-http (0.105.0) sha256=55191969d5cf61be5fbc2e0aadd51d77085318ba192a5ff5e12e3aba0826906a
|
||||
async-pool (0.12.0) sha256=5580493dc024bd704dab2ed0bf1307100722e49cbb825c7965f8501194f58506
|
||||
async-websocket (0.30.1) sha256=54bb8a8f184e4aa64434c7a78ecc55850a67a3d1dcd02e3ae787376e2c673936
|
||||
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
||||
chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe
|
||||
console (1.37.0) sha256=8093b286c2595a063849c098594fee5155ecc7967d36f3aa8cbe7569c8f3efd7
|
||||
console (1.38.0) sha256=35b82f59130e95e9be6b832d9e539c8b3988fc192e4a8e9a86ab4922f44ad5de
|
||||
cyberarm_engine (0.25.1) sha256=6f851a2ed0a32bdd25fc619172b546a8e26eefeb13a62c509f226337170d0efe
|
||||
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
|
||||
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
||||
@@ -103,17 +104,17 @@ CHECKSUMS
|
||||
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
|
||||
gosu (1.5.0-x86_64-linux)
|
||||
io-endpoint (0.18.0) sha256=720d186bcd584e8ba490bfc91dab332cc3c5ffee848b5b03894555cb275151aa
|
||||
io-event (1.21.1) sha256=b3088f89ad67edf9be9fc2f48a5fae0790e92862487551eeb337b49098eb99e6
|
||||
io-event (1.22.0) sha256=4936d38dfcf2291d897707fd94690f1a63fdb1ea3ab4fa63c6739fc6c61e1534
|
||||
io-stream (0.14.0) sha256=07d9b13ea2066ccbbc761fff8d69ad5a31e47561d31e02cc271f3ed9c1a8f1cd
|
||||
ircparser (1.0.0) sha256=0d4598f691172e2ad078207c50ba62c0d3428ae841836b0a682e6cd01d62878d
|
||||
json (3.0.1) sha256=c80f74a3570c3a310cd41c526904e7ef8363abdece4432f31fe97d812bc4a39e
|
||||
json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
|
||||
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
||||
openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2
|
||||
protocol-hpack (1.5.1) sha256=6feca238b8078da1cd295677d6f306c6001af92d75fe0643d33e6956cbc3ad91
|
||||
protocol-http (0.71.0) sha256=38577bf3df0bf0c3f7ee7e21b43fcb4bd7624e36f697dbe9c3eea344f14bd9ba
|
||||
protocol-http (0.72.0) sha256=b153474785979495b9f2df78f0fdc2e02edc7e92c1f7ea00425eed73bd02194c
|
||||
protocol-http1 (0.41.0) sha256=9863874a1228948a896c3e7ea16d13a680c8f3ed50706007c78b7033018b0ea3
|
||||
protocol-http2 (0.28.0) sha256=b7c6195dcfa80ec2484670e9fd8fc21024cc654a391aa0792433aff1188c1691
|
||||
protocol-rack (0.22.1) sha256=1185d245927ef9849a603700d6991ca353bc89724fbf98efa4a4333ed62a9fc3
|
||||
protocol-rack (0.23.0) sha256=63eec3341b20565b3dd155345fd1426ecbbed322cdc8a6accc993905388c03d7
|
||||
protocol-url (0.19.0) sha256=3f6b9f6aa742a259a0b3610fbbd9685dcd9506adf6734f428ff96259faabfb21
|
||||
protocol-websocket (0.21.2) sha256=cbe35be8e045597b221874fba5928d7cd2483884126e3e36faece4741dc88eb7
|
||||
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
|
||||
|
||||
68
lib/application_helper.rb
Normal file
68
lib/application_helper.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
module W3DHubLauncher
|
||||
class ApplicationHelper
|
||||
def self.wine_command(app)
|
||||
# FIXME: account for the cursed whitespace and wrap in quotes
|
||||
format("%s ", MemCache[:settings]&.preferences&.wine_command || "wine")
|
||||
end
|
||||
def self.wine_prefix(app)
|
||||
# TODO: fallback from app specific prefix to launcher prefix to global prefix
|
||||
# format("%s ", app&.wine_prefix_path || MemCache[:settings]&.preferences&.wine_prefix_path || "")
|
||||
|
||||
""
|
||||
end
|
||||
|
||||
def self.run(app, *args)
|
||||
exe_path = app.id == "ecw" ? format("%s/game500.exe", app.installation_path) : format("%s/game.exe", app.installation_path)
|
||||
|
||||
env = {}
|
||||
unless wine_prefix(app).to_s.empty?
|
||||
env["WINEPREFIX"] = wine_prefix(app).to_s
|
||||
end
|
||||
env["DXVK_HUD"] = "full"
|
||||
|
||||
pid = Process.spawn(
|
||||
env,
|
||||
"#{wine_command(app)}#{exe_path} -launcher #{args.join(' ')}"
|
||||
)
|
||||
|
||||
Process.detach(pid)
|
||||
end
|
||||
|
||||
def self.join_server(app, server, password: nil, token: nil, multi: false)
|
||||
parameters = []
|
||||
|
||||
parameters << format("+connect %s:%s", server.address, server.port)
|
||||
parameters << format("+netplayername \"%s\"", "cyberarm")
|
||||
parameters << format("+password \"%s\"", password) if password
|
||||
parameters << format("+userkey \"%s\"", token) if token
|
||||
parameters << "+multi" if multi
|
||||
|
||||
run(
|
||||
app,
|
||||
parameters.join(" ")
|
||||
)
|
||||
end
|
||||
|
||||
# select "best" server to join
|
||||
def self.play_now_server(app)
|
||||
channel = MemCache[:applications].find { |appl| appl.id == app.id }&.channels&.find { |c| c.id == app.channel }
|
||||
return nil unless channel
|
||||
|
||||
server_options = MemCache[:servers].select do |server|
|
||||
server.game == app.id &&
|
||||
server.channel == channel.server_channel &&
|
||||
!server.password? &&
|
||||
server.player_count < server.max_players
|
||||
end.sort_by do |s|
|
||||
[s.player_count, -s.ping]
|
||||
end.reverse
|
||||
|
||||
# try to find server with lowest ping and matching app version
|
||||
found_server = server_options.find { |s| s.version == app.version }
|
||||
# try to find server with lowest ping and undefined version
|
||||
found_server ||= server_options.find { |s| s.version == Worker::Api::GameServer::NO_OR_DEFAULT_VERSION }
|
||||
|
||||
found_server
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@ module W3DHubLauncher
|
||||
LIBRARIES = [
|
||||
Item.new("Ruby", "Programming language. A Programmer's Best Friend", "https://ruby-lang.org", "BSD 2-Clause", "https://www.ruby-lang.org/en/about/license.txt"),
|
||||
Item.new("gosu", "Light-weight game library", "https://libgosu.org", "MIT", "https://github.com/gosu/gosu/blob/master/COPYING"),
|
||||
Item.new("SDL2", "Simple DirectMedia Layer", "https://libsdl.org", "MIT", "https://github.com/libsdl-org/SDL/blob/SDL2/LICENSE.txt"),
|
||||
Item.new("SDL3", "Simple DirectMedia Layer", "https://libsdl.org", "zlib", "https://github.com/libsdl-org/SDL/blob/main/LICENSE.txt"),
|
||||
Item.new("MojoAL", "OpenAL sound library implementation in a single C file", "https://icculus.org/mojoAL/", "MIT", "https://github.com/icculus/mojoAL/blob/main/LICENSE.txt"),
|
||||
|
||||
Item.new("async", "Asynchronous event-driven reactor for Ruby", "https://github.com/socketry/async", "MIT", "https://github.com/socketry/async/blob/main/license.md"),
|
||||
|
||||
280
lib/dds.rb
Normal file
280
lib/dds.rb
Normal file
@@ -0,0 +1,280 @@
|
||||
require "stringio"
|
||||
|
||||
module W3DHubLauncher
|
||||
class DDS
|
||||
# https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-header
|
||||
# https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-pixelformat
|
||||
# https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dx-graphics-dds-pguide
|
||||
# https://docs.microsoft.com/en-us/windows/win32/direct3ddds/dds-file-layout-for-textures
|
||||
|
||||
DXT1 = 827611204
|
||||
DXT2 = 844388420
|
||||
DXT3 = 861165636
|
||||
DXT4 = 877942852
|
||||
DXT5 = 894720068
|
||||
|
||||
DDSD_CAPS = 0x1
|
||||
DDSD_HEIGHT = 0x2
|
||||
DDSD_WIDTH = 0x4
|
||||
DDSD_PITCH = 0x8
|
||||
DDSD_PIXELFORMAT = 0x1000
|
||||
DDSD_MIPMAPCOUNT = 0x20_000
|
||||
DDSD_LINEARSIZE = 0x80_000
|
||||
DDSD_DEPTH = 0x800_000
|
||||
|
||||
DDSCAPS_COMPLEX = 0x8
|
||||
DDSCAPS_MIPMAP = 0x400_000
|
||||
DDSCAPS_TEXTURE = 0x1000
|
||||
|
||||
DDSCAPS2_CUBEMAP = 0x200
|
||||
DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
|
||||
DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
|
||||
DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
|
||||
DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
|
||||
DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
|
||||
DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
|
||||
DDSCAPS2_VOLUME = 0x200_000
|
||||
|
||||
DDPF_ALPHAPIXELS = 0x1
|
||||
DDPF_ALPHA = 0x2
|
||||
DDPF_FOURCC = 0x4
|
||||
DDPF_RGB = 0x40
|
||||
DDPF_YUV = 0x200
|
||||
DDPF_LUMINANCE = 0x20_000
|
||||
|
||||
Header = Struct.new(
|
||||
:byte_size, :flags, :height, :width, :pitch_or_linear_size,
|
||||
:depth, :mipmap_count, :reserved1, :pixel_format,
|
||||
:caps, :caps2, :caps3, :caps4, :reserved2
|
||||
)
|
||||
|
||||
HeaderX10 = Struct.new(:value)
|
||||
|
||||
PixelFormat = Struct.new(
|
||||
:byte_size, :flags, :four_cc, :rgb_bit_count,
|
||||
:red_bit_mask, :green_bit_mask, :blue_bit_mask, :alpha_bit_mask
|
||||
)
|
||||
|
||||
Image = Struct.new(:data, :width, :height)
|
||||
|
||||
attr_reader :header, :images
|
||||
def initialize(path: nil, io: nil, shallow: true)
|
||||
if path
|
||||
@data = StringIO.new(File.binread(path))
|
||||
elsif io
|
||||
@data = io
|
||||
else
|
||||
raise "Nah man."
|
||||
end
|
||||
|
||||
@shallow = shallow
|
||||
@images = []
|
||||
|
||||
decode_header
|
||||
decode_images
|
||||
end
|
||||
|
||||
# typedef struct {
|
||||
# DWORD dwSize;
|
||||
# DWORD dwFlags;
|
||||
# DWORD dwHeight;
|
||||
# DWORD dwWidth;
|
||||
# DWORD dwPitchOrLinearSize;
|
||||
# DWORD dwDepth;
|
||||
# DWORD dwMipMapCount;
|
||||
# DWORD dwReserved1[11];
|
||||
# DDS_PIXELFORMAT ddspf;
|
||||
# DWORD dwCaps;
|
||||
# DWORD dwCaps2;
|
||||
# DWORD dwCaps3;
|
||||
# DWORD dwCaps4;
|
||||
# DWORD dwReserved2;
|
||||
# } DDS_HEADER;
|
||||
|
||||
def decode_header
|
||||
@data.pos = 0
|
||||
|
||||
raise "File is not a DDS" unless read_u32 == 0x20534444 # magic byte "DDS"
|
||||
|
||||
size = read_u32
|
||||
flags = read_u32
|
||||
height = read_u32
|
||||
width = read_u32
|
||||
pitch_or_linear_size = read_u32
|
||||
depth = read_u32
|
||||
mipmap_count = read_u32
|
||||
reserved1 = 11.times.map { read_u32 }
|
||||
|
||||
pixel_format = read_pixelformat
|
||||
|
||||
caps = read_u32
|
||||
caps2 = read_u32
|
||||
caps3 = read_u32
|
||||
caps4 = read_u32
|
||||
reserved2 = read_u32
|
||||
|
||||
@header = Header.new(
|
||||
size, flags, height, width, pitch_or_linear_size, depth, mipmap_count,
|
||||
reserved1, pixel_format, caps, caps2, caps3, caps4, reserved2
|
||||
)
|
||||
|
||||
@header.freeze
|
||||
|
||||
raise "Invalid dds header size: #{size}, expected 124" unless size == 124
|
||||
raise "Invalid pixel format header size: #{pixel_format.byte_size}, expected 32" unless pixel_format.byte_size == 32
|
||||
end
|
||||
|
||||
def decode_images
|
||||
pixel_format = @header.pixel_format
|
||||
|
||||
raise "Unsupported dds format: #{pixel_format.four_cc}" unless (pixel_format.four_cc == DXT1 || pixel_format.four_cc == DXT3 || pixel_format.four_cc == DXT5)
|
||||
raise "Unsupported pixel format: #{pixel_format.flags}" unless pixel_format.flags == DDPF_FOURCC
|
||||
|
||||
width = @header.width
|
||||
height = @header.height
|
||||
mipmap_count = [1, @header.mipmap_count].max
|
||||
mipmap_count = 1 if @shallow
|
||||
block_bytes = 8
|
||||
block_bytes = 16 if pixel_format.four_cc == DXT3 || pixel_format.four_cc == DXT5
|
||||
data_offset = @header.byte_size + 4
|
||||
|
||||
mipmap_count.times do
|
||||
data_length = [4, width].max / 4 * [4, height].max / 4 * block_bytes
|
||||
|
||||
@data.pos = data_offset
|
||||
image = Image.new(
|
||||
to_rgba(@data.read(data_length), width, height, pixel_format.four_cc == DXT1),
|
||||
width,
|
||||
height
|
||||
)
|
||||
|
||||
@images << image
|
||||
|
||||
data_offset += data_length
|
||||
width /= 2
|
||||
height /= 2
|
||||
end
|
||||
end
|
||||
|
||||
def read_u32
|
||||
@data.read(4).unpack1("L")
|
||||
end
|
||||
|
||||
# struct DDS_PIXELFORMAT {
|
||||
# DWORD dwSize;
|
||||
# DWORD dwFlags;
|
||||
# DWORD dwFourCC;
|
||||
# DWORD dwRGBBitCount;
|
||||
# DWORD dwRBitMask;
|
||||
# DWORD dwGBitMask;
|
||||
# DWORD dwBBitMask;
|
||||
# DWORD dwABitMask;
|
||||
# };
|
||||
|
||||
def read_pixelformat
|
||||
|
||||
size = read_u32
|
||||
flags = read_u32
|
||||
four_cc = read_u32
|
||||
rgb_bit_count = read_u32
|
||||
red_bit_mask = read_u32
|
||||
green_bit_mask = read_u32
|
||||
blue_bit_mask = read_u32
|
||||
alpha_bit_mask = read_u32
|
||||
|
||||
PixelFormat.new(
|
||||
size, flags, four_cc, rgb_bit_count,
|
||||
red_bit_mask, green_bit_mask, blue_bit_mask, alpha_bit_mask
|
||||
)
|
||||
end
|
||||
|
||||
# https://github.com/kchapelier/decode-dxt/
|
||||
def to_rgba(data, width, height, dxt1 = false)
|
||||
conversion_started = Gosu.milliseconds
|
||||
|
||||
data = StringIO.new(data)
|
||||
rgba = Array.new(width * height * 4, 0) # StringIO.new("\0" * (width * height * 4), "wb") #
|
||||
width_4 = (width / 4) | 0
|
||||
height_4 = (height / 4) | 0
|
||||
stride = 4
|
||||
|
||||
height_4.times do |h|
|
||||
width_4.times do |w|
|
||||
a, b, index = data.read(8).unpack("vvV")
|
||||
color_values = interpolate_color_values(a, b, dxt1)
|
||||
color_indices = index
|
||||
|
||||
4.times do |y|
|
||||
4.times do |x|
|
||||
pixel_index = (3 - x) + (y * 4)
|
||||
rgba_index = (h * 4 + 3 - y) * width * 4 + (w * 4 + x) * 4
|
||||
color_index = (color_indices >> (2 * (15 - pixel_index))) & 0x03
|
||||
|
||||
rgba[rgba_index] = color_values[color_index * 4]
|
||||
rgba[rgba_index + 1] = color_values[color_index * 4 + 1]
|
||||
rgba[rgba_index + 2] = color_values[color_index * 4 + 2]
|
||||
rgba[rgba_index + 3] = color_values[color_index * 4 + 3]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
puts "Inital set complete after: #{Gosu.milliseconds - conversion_started}ms"
|
||||
v = rgba.pack("C*") #.map { |v| v.chr }.join
|
||||
puts "type set complete after: #{Gosu.milliseconds - conversion_started}ms"
|
||||
|
||||
v
|
||||
end
|
||||
|
||||
def interpolate_color_values(a, b, dxt1 = false)
|
||||
first_color = convert_565_byte_to_rgb(a)
|
||||
second_color = convert_565_byte_to_rgb(b)
|
||||
|
||||
color_values = [
|
||||
first_color[0], first_color[1], first_color[2], 255,
|
||||
second_color[0], second_color[1], second_color[2], 255
|
||||
]
|
||||
|
||||
if (dxt1 && a <= b)
|
||||
color_values.push(
|
||||
(first_color[0] + second_color[0]) / 2,
|
||||
(first_color[1] + second_color[1]) / 2,
|
||||
(first_color[2] + second_color[2]) / 2,
|
||||
255,
|
||||
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
)
|
||||
else
|
||||
color_values.push(
|
||||
lerp(first_color[0], second_color[0], 1 / 3),
|
||||
lerp(first_color[1], second_color[1], 1 / 3),
|
||||
lerp(first_color[2], second_color[2], 1 / 3),
|
||||
255,
|
||||
|
||||
lerp(first_color[0], second_color[0], 2 / 3),
|
||||
lerp(first_color[1], second_color[1], 2 / 3),
|
||||
lerp(first_color[2], second_color[2], 2 / 3),
|
||||
255
|
||||
)
|
||||
end
|
||||
|
||||
color_values
|
||||
end
|
||||
|
||||
def convert_565_byte_to_rgb(byte)
|
||||
[
|
||||
((byte >> 11) & 31) * (255 / 31),
|
||||
((byte >> 5) & 63) * (255 / 63),
|
||||
(byte & 31) * (255 / 31)
|
||||
]
|
||||
end
|
||||
|
||||
def lerp(a, b, r)
|
||||
a * (1 - r) + b * r
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -13,7 +13,7 @@ module W3DHubLauncher
|
||||
title NAME
|
||||
tagline format("v%s (%s)", VERSION, VERSION_NAME), margin_left: PADDING
|
||||
para "© 2021 - #{Time.now.year} cyberarm", margin_left: PADDING
|
||||
link "MIT licence", margin_left: PADDING
|
||||
link "MIT license", margin_left: PADDING
|
||||
|
||||
title "Special Thanks", margin_top: LARGE_PADDING
|
||||
W3DHubLauncher::Attribution::SPECIAL_THANKS.each do |item|
|
||||
@@ -40,12 +40,18 @@ module W3DHubLauncher
|
||||
|
||||
def present_item(item)
|
||||
unless item.url.empty?
|
||||
link item.name, tip: item.url, margin_left: PADDING, font: FONT_BOLD
|
||||
link item.name, tip: item.url, margin_left: PADDING, font: FONT_BOLD do
|
||||
SDL.OpenURL(item.url)
|
||||
end
|
||||
else
|
||||
tagline item.name, margin_left: PADDING
|
||||
end
|
||||
para item.description, margin_left: LARGE_PADDING
|
||||
link item.license, tip: item.license_url, margin_left: PADDING + LARGE_PADDING unless item.license.empty?
|
||||
unless item.license.empty?
|
||||
link format("%s license", item.license), tip: item.license_url, margin_left: PADDING + LARGE_PADDING do
|
||||
SDL.OpenURL(item.license_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
77
lib/dialogs/import_application.rb
Normal file
77
lib/dialogs/import_application.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
module W3DHubLauncher
|
||||
class Dialog
|
||||
class ImportApplication < W3DHubLauncher::Dialog
|
||||
def setup
|
||||
super
|
||||
|
||||
@application = @options[:application]
|
||||
@channel = @options[:channel]
|
||||
|
||||
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: 0xee_63452c) do
|
||||
flow(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0) do
|
||||
banner "Import Application", width: 1.0, text_align: :center
|
||||
end
|
||||
|
||||
stack(width: 1.0, fill: true, scroll: true, margin_left: PADDING, margin_right: PADDING, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
|
||||
title format("%s (%s)", @application.name, @channel.name)
|
||||
caption "Path to application executable:"
|
||||
@path_input = edit_line "", width: 1.0
|
||||
button "Browse..." do
|
||||
Gosu.choose_file do |file_list|
|
||||
pp file_list
|
||||
|
||||
unless file_list.empty?
|
||||
@path_input.value = file_list.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@path_input.subscribe(:changed) do |e, value|
|
||||
found = false
|
||||
|
||||
if File.directory?(File.dirname(value))
|
||||
Dir.glob("#{File.dirname(value)}/game*.exe").each do |file|
|
||||
if File.exist?(file)
|
||||
found = true
|
||||
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@import_button.enabled = found
|
||||
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: 0) do
|
||||
button "Close" do
|
||||
pop_state
|
||||
end
|
||||
|
||||
flow(fill: true)
|
||||
|
||||
@import_button = button "Import", enabled: false, **CTA_BUTTON_THEME do
|
||||
pop_state
|
||||
|
||||
app = Worker::Api::Settings::Application.create(
|
||||
id: @application.id,
|
||||
channel: @channel.id,
|
||||
version: @channel.version,
|
||||
installation_path: File.dirname(@path_input.value),
|
||||
wine_prefix_path: "",
|
||||
launch_command: "%COMMAND%",
|
||||
timestamp: Time.now.to_i
|
||||
)
|
||||
|
||||
MemCache[:settings].applications << app
|
||||
|
||||
Worker::Api.update_settings(MemCache[:settings])
|
||||
|
||||
# FIXME: broadcast app installed on CyberarmEngine::EventBus
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@ module W3DHubLauncher
|
||||
BLACK_IMAGE = Gosu.render(64, 64, retro: true) { Gosu.draw_rect(0, 0, 32, 32, Gosu::Color::BLACK) }
|
||||
WHITE_IMAGE = Gosu.render(64, 64, retro: true) { Gosu.draw_rect(0, 0, 32, 32, Gosu::Color::WHITE) }
|
||||
|
||||
def safe_get_image(path, retro: false)
|
||||
def safe_get_image(path, fallback_path: "#{ROOT_PATH}/media/default.png", retro: false)
|
||||
raise RuntimeError, "Images may only be loaded from the main thread!" unless Thread.current == Thread.main
|
||||
|
||||
begin
|
||||
@@ -12,8 +12,11 @@ module W3DHubLauncher
|
||||
pp e
|
||||
end
|
||||
|
||||
path = "#{ROOT_PATH}/media/default.png"
|
||||
return get_image(path, retro: retro) if File.exist?(path)
|
||||
begin
|
||||
return get_image(fallback_path, retro: retro) if File.exist?(fallback_path)
|
||||
rescue RuntimeError => e
|
||||
pp e
|
||||
end
|
||||
|
||||
WHITE_IMAGE
|
||||
end
|
||||
|
||||
6
lib/hardware_information.rb
Normal file
6
lib/hardware_information.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
module W3DHubLauncher
|
||||
class HardwareInformation
|
||||
def initialize
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,13 +9,18 @@ module W3DHubLauncher
|
||||
@current_app = @games.first
|
||||
@current_channel = @current_app&.channels&.first
|
||||
|
||||
@display_mode = :all_games
|
||||
|
||||
# game bar container
|
||||
flow(width: 1.0, height: 60) do
|
||||
widget(width: 220, height: 1.0, style_class: [:all_button]) do
|
||||
flow(width: 1.0, height: 40, margin_left: PADDING, v_align: :center, h_align: :center) do
|
||||
widget(width: 220, height: 1.0, style_class: [:all_button]) do |w|
|
||||
flow(width: 1.0, height: 40, margin_left: PADDING, v_align: :center) do
|
||||
image safe_get_image("#{ROOT_PATH}/media/icons/menuGrid.png"), height: 40, color: 0xff_bbbbbb
|
||||
link "ALL GAMES", text_size: 24, font: FONT_BLACK, height: 1.0, text_v_align: :center
|
||||
end
|
||||
w.subscribe(:clicked_left_mouse_button) do
|
||||
populate_all_games
|
||||
end
|
||||
end
|
||||
|
||||
@games_list_container = flow(fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY, margin_left: PADDING) do
|
||||
@@ -23,26 +28,70 @@ module W3DHubLauncher
|
||||
end
|
||||
|
||||
# game content container
|
||||
flow(width: 1.0, fill: true, margin_top: LARGE_PADDING) do
|
||||
# game info container
|
||||
@game_info_container = stack(width: 340, height: 1.0) do
|
||||
end
|
||||
|
||||
# game events and news container
|
||||
stack(fill: true, height: 1.0, margin_left: LARGE_PADDING, scroll: true) do
|
||||
@event_container = flow(width: 1.0, height: 1.0, max_height: 380, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||
end
|
||||
|
||||
# news container
|
||||
@news_container = flow(width: 1.0, margin_top: PADDING) do
|
||||
end
|
||||
end
|
||||
@game_content_container = flow(width: 1.0, fill: true, margin_top: LARGE_PADDING) do
|
||||
end
|
||||
|
||||
populate_game
|
||||
end
|
||||
|
||||
def focus
|
||||
@parent.show_battleview_panel
|
||||
end
|
||||
|
||||
def blur
|
||||
@parent.hide_battleview_panel
|
||||
end
|
||||
|
||||
def populate_game_content_container
|
||||
@game_content_container.clear do
|
||||
# game info container
|
||||
@game_info_container = stack(width: 340, height: 1.0) do
|
||||
end
|
||||
|
||||
# game events and news container
|
||||
stack(fill: true, height: 1.0, margin_left: PADDING, scroll: true) do
|
||||
@event_container = stack(width: 1.0, padding: PADDING, margin_left: PADDING, margin_bottom: PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0x88_26a269) do
|
||||
end
|
||||
|
||||
# news container
|
||||
@news_container = flow(width: 1.0) do
|
||||
end
|
||||
|
||||
# "dynamically" adjust news item widths
|
||||
@news_container.subscribe(:size_changed) do |event|
|
||||
@news_container.children.each do |box|
|
||||
box.style.width = 1.0 / news_item_width_ratio
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def populate_all_games
|
||||
@display_mode = :all_games
|
||||
|
||||
@game_content_container.clear do
|
||||
flow(fill: true, height: 1.0, scroll: true, tag: :hi_mom) do |e|
|
||||
@games.each do |app|
|
||||
image safe_get_image("#{ROOT_PATH}/media/default_game_cover.png"), aspect_ratio: 3 / 4.0, width: 1.0 / game_cover_width_ratio(e), margin_left: HALF_PADDING, margin_right: HALF_PADDING, margin_bottom: PADDING, tip: app.name
|
||||
end
|
||||
|
||||
e.subscribe(:size_changed) do |e|
|
||||
e.children.each do |child|
|
||||
child.style.width = 1.0 / game_cover_width_ratio(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def populate_game(game = @current_app, channel = @current_channel)
|
||||
if @display_mode == :all_games
|
||||
@display_mode = :game
|
||||
|
||||
populate_game_content_container
|
||||
end
|
||||
|
||||
@current_app = game
|
||||
@current_channel = game&.channels&.first
|
||||
|
||||
@@ -62,6 +111,29 @@ module W3DHubLauncher
|
||||
return
|
||||
end
|
||||
|
||||
app_id = @current_app.id
|
||||
unless MemCache[:"events_#{app_id}"]
|
||||
Worker::Api.events(app_id) do |result|
|
||||
if result.okay?
|
||||
File.write("events_#{app_id}.json", result.data)
|
||||
MemCache[:"events_#{app_id}"] = JSON.parse(result.data)&.map { |item| Worker::Api::ServerEvent.new(item) } || []
|
||||
|
||||
populate_game_event if app_id == @current_app.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless MemCache[:"news_#{app_id}"]
|
||||
Worker::Api.news(app_id) do |result|
|
||||
if result.okay?
|
||||
File.write("news_#{app_id}.json", result.data)
|
||||
MemCache[:"news_#{app_id}"] = JSON.parse(result.data)["news"]&.map { |item| Worker::Api::NewsItem.new(item) } || []
|
||||
|
||||
populate_game_news if app_id == @current_app.id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
populate_games_list
|
||||
populate_game_info
|
||||
populate_game_event
|
||||
@@ -70,7 +142,7 @@ module W3DHubLauncher
|
||||
|
||||
def populate_games_list
|
||||
@games_list_container.clear do
|
||||
@games.each_with_index do |game, i|
|
||||
@games.each do |game|
|
||||
btn = button(safe_get_image("#{CACHE_PATH}/icon_#{game.id}.png"), tag: :"image_icon_#{game.id}", style_class: [:app_icon_button], enabled: @current_app.id != game.id, tip: game.name) do |btn|
|
||||
populate_game(game, game&.channels&.first)
|
||||
end
|
||||
@@ -80,14 +152,11 @@ module W3DHubLauncher
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
def populate_game_info
|
||||
# FIXME: We should be able to query an api to figure out the installation state, instead of explictly writing it out like this...
|
||||
application = MemCache[:settings].applications.find { |app| app.id == @current_app.id }
|
||||
application = MemCache[:settings].application_installed?(@current_app, @current_channel)
|
||||
# pp application
|
||||
|
||||
@game_info_container.clear do
|
||||
@@ -100,7 +169,9 @@ module W3DHubLauncher
|
||||
# web links
|
||||
stack(width: 1.0, fill: true, padding: 0, padding_top: LARGE_PADDING) do
|
||||
@current_app.web_links.each do |link|
|
||||
link link.name, text_size: 24, tip: link.uri
|
||||
link link.name, text_size: 24, tip: link.uri do
|
||||
SDL.OpenURL(link.uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,57 +183,136 @@ module W3DHubLauncher
|
||||
populate_game_info
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, height: 60) do
|
||||
if application
|
||||
button "JOIN", tip: "Join most populated or lowest ping server", fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_LEFT, **CTA_BUTTON_THEME
|
||||
button safe_get_image("#{ROOT_PATH}/media/icons/singleplayer.png"), tip: "Single player", image_height: 1.0, background_nine_slice: NINE_SLICE_SQUARE, **CTA_BUTTON_THEME
|
||||
button safe_get_image("#{ROOT_PATH}/media/icons/gear.png"), tip: "Options", image_height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_RIGHT, **CTA_BUTTON_THEME
|
||||
button "JOIN", tip: "Join most populated or lowest ping server", fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_LEFT, **CTA_BUTTON_THEME do |btn|
|
||||
server = ApplicationHelper.play_now_server(application)
|
||||
|
||||
if server
|
||||
ApplicationHelper.join_server(application, server)
|
||||
else
|
||||
puts "No server available."
|
||||
end
|
||||
end
|
||||
button safe_get_image("#{ROOT_PATH}/media/icons/singleplayer.png"), tip: "Single player", image_height: 1.0, background_nine_slice: NINE_SLICE_SQUARE, **CTA_BUTTON_THEME do
|
||||
ApplicationHelper.run(application)
|
||||
end
|
||||
button safe_get_image("#{ROOT_PATH}/media/icons/gear.png"), tip: "Options", image_height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_RIGHT, **CTA_BUTTON_THEME do |btn|
|
||||
menu(parent: btn) do
|
||||
menu_item("v#{application.version}", enabled: false, disabled: { color: 0xdd_ffffff }, tip: "Installed application version")
|
||||
stack(width: 1.0, height: 2, background: 0xff_bbbbbb)
|
||||
menu_item("Installation Folder", tip: "Open application installation folder:\n#{application.installation_path}")
|
||||
menu_item("Screenshots Folder", tip: "Open application installation folder:\n#{Dir.home}/Documents/W3D Hub/#{application.id}-#{application.channel}/Screenshots")
|
||||
stack(width: 1.0, height: 2, background: 0xff_bbbbbb)
|
||||
menu_item("Application Settings", tip: "Edit application launch options")
|
||||
menu_item("Repair Installation", tip: "Attempt to fix and repair installation")
|
||||
menu_item("Check for Updates", tip: "Manually check for application updates")
|
||||
stack(width: 1.0, height: 2, background: 0xff_bbbbbb)
|
||||
menu_item("Move Installation", tip: "Move application installation to a different directory or disk")
|
||||
menu_item("Unlink Installation", tip: "The application will be removed from the launcher\nbut the application's files will not be touched")
|
||||
menu_item("Uninstall", tip: "Uninstall application and delete files")
|
||||
end.show
|
||||
end
|
||||
elsif @current_app.servicable?
|
||||
# pp @current_app
|
||||
button "Import", enabled: false, tip: "Import existing application installation", fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_LEFT, **CTA_BUTTON_THEME
|
||||
button "Download", tip: "Download and install application", fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_RIGHT, **CTA_BUTTON_THEME
|
||||
button "Import", tip: "Import existing application installation", fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_LEFT, **CTA_BUTTON_THEME do |btn|
|
||||
dialog(Dialog::ImportApplication, application: @current_app, channel: @current_channel)
|
||||
end
|
||||
button "Download", tip: "Download and install application", fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_RIGHT, **CTA_BUTTON_THEME do |btn|
|
||||
btn.enabled = false
|
||||
|
||||
Worker::Api.install_application(@current_app.id, @current_channel.id) do |status, result|
|
||||
handle_task_update(status, result)
|
||||
end
|
||||
end
|
||||
else
|
||||
button "Import", enabled: false, tip: "Import existing application installation", fill: true, height: 1.0, **CTA_BUTTON_THEME
|
||||
end
|
||||
end
|
||||
# FIXME: the reported version should be the _installed_ version, not the current channel version
|
||||
inscription "Version: #{@current_channel.version}", margin_top: PADDING, tip: "Installed version" if application
|
||||
end
|
||||
end
|
||||
|
||||
def populate_game_event
|
||||
# TODO: Hide event container if there is no event
|
||||
@event_container.show if false
|
||||
@event_container.hide if true
|
||||
app_events = MemCache[:"events_#{@current_app.id}"] || []
|
||||
if app_events.empty?
|
||||
@event_container.hide
|
||||
else
|
||||
@event_container.show
|
||||
|
||||
@event_container.clear do
|
||||
image safe_get_image("#{ROOT_PATH}/media/background.png"), fill: true, aspect_ratio: 16.0 / 9.0
|
||||
event = app_events.sort(&:start_time).last
|
||||
|
||||
stack(fill: true, height: 1.0, margin_left: PADDING) do
|
||||
caption "Upcoming Event".upcase, color: 0xff_22aa11
|
||||
title "Red Alert: A Path Beyond Game Night"
|
||||
tagline "July 11, 2028"
|
||||
|
||||
flow(fill: true)
|
||||
|
||||
button "Read More", margin_left: PADDING, margin_right: LARGE_PADDING, margin_bottom: PADDING, width: 1.0
|
||||
@event_container.clear do
|
||||
caption "Upcoming Event".upcase, color: 0xaa_ffffff
|
||||
tagline event.title # "Red Alert: A Path Beyond Game Night"
|
||||
caption event.start_time.strftime("%B %e, %Y • %T") # "July 11, 2028"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def populate_game_news
|
||||
app_news = MemCache[:"news_#{@current_app.id}"] || []
|
||||
|
||||
@news_container.clear do
|
||||
9.times do
|
||||
stack(width: 1.0 / 3, min_width: 345, height: 345, aspect_ratio: 1, margin_left: HALF_PADDING, margin_right: HALF_PADDING, margin_bottom: PADDING) do
|
||||
stack(width: 1.0, fill: true, background_image: safe_get_image("#{ROOT_PATH}/media/background.png"), background_image_mode: :fill)
|
||||
stack(width: 1.0, height: 1.0 / 3, padding: PADDING, v_align: :bottom, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY, border_thickness_top: 1, border_color_top: Gosu::Color::BLACK) do
|
||||
caption "NEWS", color: 0x88_ffffff
|
||||
tagline "A News Item Post A News Item Post"
|
||||
app_news.each do |item|
|
||||
stack(width: 1.0 / news_item_width_ratio, height: 345, aspect_ratio: 1, margin_left: PADDING, margin_bottom: PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||
stack(width: 1.0, height: 1.0 / 2.5, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||
para item.timestamp.strftime("%B %e, %Y") #"September 29, 2026"
|
||||
tagline item.title
|
||||
end
|
||||
|
||||
stack(width: 1.0, fill: true, padding: PADDING, padding_bottom: 0, scroll: false) do
|
||||
para item.blurb
|
||||
end
|
||||
|
||||
button "Read More", margin: PADDING, width: 1.0, tip: item.uri do
|
||||
SDL.OpenURL(item.uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def game_cover_width_ratio(e)
|
||||
(e.width / 300.0).round.clamp(1..10)
|
||||
end
|
||||
|
||||
def news_item_width_ratio
|
||||
(@news_container.width / 400.0).round.clamp(1..10)
|
||||
end
|
||||
|
||||
def handle_task_update(result, status)
|
||||
pp [result, status]
|
||||
|
||||
status_bar = current_state.application_task_status_bar_container
|
||||
status_bar_title = find_element_by_tag(status_bar, :status_bar_title)
|
||||
status_bar_label = find_element_by_tag(status_bar, :status_bar_label)
|
||||
status_bar_progress = find_element_by_tag(status_bar, :status_bar_progress)
|
||||
|
||||
data = result.data
|
||||
|
||||
case status
|
||||
when Worker::Request::STATUS_COMPLETE
|
||||
status_bar.hide
|
||||
|
||||
MemCache[:settings].applications << Worker::Api::Settings::Application.new(data["application"])
|
||||
Worker::Api.update_settings(MemCache[:settings])
|
||||
|
||||
populate_game_info
|
||||
when Worker::Request::STATUS_IN_PROGRESS
|
||||
status_bar.show
|
||||
|
||||
status_bar_title.value = data["status"]["title"]
|
||||
status_bar_label.value = data["status"]["label"]
|
||||
status_bar_progress.value = data["status"]["fraction"]
|
||||
when Worker::Request::STATUS_ERROR
|
||||
status_bar.hide
|
||||
|
||||
populate_game_info
|
||||
|
||||
# FIXME: Present error to player
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,27 +6,26 @@ module W3DHubLauncher
|
||||
def setup
|
||||
@games_filter = []
|
||||
|
||||
CyberarmEngine::EventBus.subscribe("game_server_pings", self, :handle_game_server_pings)
|
||||
|
||||
# game bar container
|
||||
flow(width: 1.0, height: 60) do
|
||||
widget(width: 220, height: 1.0, style_class: [:all_button]) do |w|
|
||||
flow(width: 1.0, height: 40, margin_left: PADDING, v_align: :center, h_align: :center) do
|
||||
flow(width: 1.0, height: 40, margin_left: PADDING, v_align: :center) do
|
||||
image safe_get_image("#{ROOT_PATH}/media/icons/menuGrid.png"), height: 40, color: 0xff_bbbbbb
|
||||
link "ALL SERVERS", text_size: 24, font: FONT_BLACK, height: 1.0, text_v_align: :center
|
||||
end
|
||||
|
||||
w.subscribe(:clicked_left_mouse_button) do
|
||||
@games_filter.clear
|
||||
@app_filters_container.children.each { |c| c.enabled = true }
|
||||
populate_server_list
|
||||
end
|
||||
end
|
||||
|
||||
flow(fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY, margin_left: PADDING) do
|
||||
@app_filters_container = flow(fill: true, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY, margin_left: PADDING) do
|
||||
MemCache[:applications].each do |app|
|
||||
next unless app.game?
|
||||
|
||||
button(safe_get_image("#{CACHE_PATH}/icon_#{app.id}.png"), style_class: [:app_icon_button], tip: app.name) do |btn|
|
||||
button(safe_get_image("#{CACHE_PATH}/icon_#{app.id}.png"), style_class: [:app_icon_button], tag: :"app_filter_#{app.id}", tip: app.name) do |btn|
|
||||
if shift_down?
|
||||
@games_filter << app.id
|
||||
@games_filter.uniq!
|
||||
@@ -35,6 +34,10 @@ module W3DHubLauncher
|
||||
@games_filter << app.id
|
||||
end
|
||||
|
||||
buttons = btn.parent.children
|
||||
filters = @games_filter.map { |f| :"app_filter_#{f}" }
|
||||
buttons.each { |e| e.enabled = !filters.include?(e.style.tag) }
|
||||
|
||||
btn.enabled = false
|
||||
|
||||
populate_server_list
|
||||
@@ -51,8 +54,16 @@ module W3DHubLauncher
|
||||
populate_server_list
|
||||
end
|
||||
|
||||
def focus
|
||||
CyberarmEngine::EventBus.subscribe("game_server_pings", self, :handle_game_server_pings)
|
||||
|
||||
@parent.show_server_details_panel
|
||||
end
|
||||
|
||||
def blur
|
||||
CyberarmEngine::EventBus.unsubscribe("game_server_pings", self)
|
||||
|
||||
@parent.hide_server_details_panel
|
||||
end
|
||||
|
||||
def handle_game_server_pings(payload)
|
||||
@@ -64,13 +75,13 @@ module W3DHubLauncher
|
||||
(MemCache[:servers] || []).select { |s| @games_filter.empty? ? true : @games_filter.include?(s.game) }.sort_by { |s| [s.player_count, -s.ping] }.reverse.each do |server|
|
||||
app = MemCache[:applications].find { |a| a.id == server.game }
|
||||
|
||||
widget(width: 1.0, height: 48, padding_top: HALF_PADDING, padding_bottom: HALF_PADDING, margin_bottom: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: server.channel == "release" ? ALPHA_GRAY : 0xaa_c64600, hover: { background_nine_slice_color: 0xff_5e5c64 } , active: { background_nine_slice_color: 0xaa_5e5c64 }) do
|
||||
widget(width: 1.0, height: 56 + PADDING, padding_top: HALF_PADDING, padding_bottom: HALF_PADDING, margin_bottom: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: server.channel == "release" ? ALPHA_GRAY : 0xaa_c64600, hover: { background_nine_slice_color: 0xff_5e5c64 } , active: { background_nine_slice_color: 0xaa_5e5c64 }) do |w|
|
||||
# app icon container
|
||||
image(safe_get_image("#{CACHE_PATH}/icon_#{app.id}.png"), tip: app.name, width: 48, height: 1.0, margin_left: HALF_PADDING)
|
||||
image(safe_get_image("#{CACHE_PATH}/icon_#{app.id}.png"), tip: app.name, width: 56, height: 1.0, margin_left: HALF_PADDING)
|
||||
|
||||
# server name, region, and times container
|
||||
stack(fill: true, height: 1.0, margin_left: HALF_PADDING) do
|
||||
stack(v_align: :center) do
|
||||
stack(width: 1.0, v_align: :center) do
|
||||
# server name
|
||||
caption server.name, text_wrap: :none, tip: server.name
|
||||
# server info
|
||||
@@ -97,11 +108,15 @@ module W3DHubLauncher
|
||||
# server ping
|
||||
flow(width: 96, height: 1.0, margin_left: HALF_PADDING, margin_right: HALF_PADDING) do
|
||||
stack(fill: true, height: 1.0, v_align: :center) do
|
||||
caption server.ping == Worker::Api::GameServer::BAD_OR_UNKNOWN_PING ? "?" : server.ping, width: 1.0, text_align: :center, tip: server.ping == Worker::Api::GameServer::BAD_OR_UNKNOWN_PING ? "Server has not replied yet or did not reply to ICMP Echo Request.\nPing unknown." : ""
|
||||
caption server.ping == Worker::Api::GameServer::BAD_OR_UNKNOWN_PING ? "?" : format("%ims", server.ping), width: 1.0, text_align: :center, tip: server.ping == Worker::Api::GameServer::BAD_OR_UNKNOWN_PING ? "Server has not replied yet or did not reply to ICMP Echo Request.\nPing unknown." : ""
|
||||
inscription "ping", text_wrap: :none, width: 1.0, text_align: :center, margin_top: -HALF_PADDING
|
||||
end
|
||||
stack(width: 8, height: server.ping_score_ratio, v_align: :center, min_height: 8, background_nine_slice: NINE_SLICE_ROUNDED_SMALL, background_nine_slice_from_edge: NINE_SLICE_EDGE_SMALL, background_nine_slice_color: server.ping_score_color)
|
||||
end
|
||||
|
||||
w.subscribe(:clicked_left_mouse_button) do
|
||||
@parent.populate_server_information(server)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module W3DHubLauncher
|
||||
class States
|
||||
class Interface < W3DHubLauncher::State
|
||||
attr_reader :application_task_status_bar_container
|
||||
|
||||
def setup
|
||||
super
|
||||
|
||||
@@ -11,7 +13,7 @@ module W3DHubLauncher
|
||||
# content container
|
||||
stack(width: 1.0, fill: true, margin: PADDING) do
|
||||
# header bar container
|
||||
flow(width: 1.0, height: 80, margin_bottom: PADDING) do |c|
|
||||
flow(width: 1.0, height: 100, margin_bottom: PADDING) do |c|
|
||||
# logo + menu button
|
||||
button(safe_get_image("#{ROOT_PATH}/media/logo.png"), image_height: 1.0, background: 0, border_color: 0, hover: { background: 0 }, active: { background: 0, color: 0xff_ffffff }) do |btn|
|
||||
menu(parent: btn) do
|
||||
@@ -30,9 +32,9 @@ module W3DHubLauncher
|
||||
# stack(fill: true)
|
||||
|
||||
# navigation container
|
||||
flow(width: 1.0) do
|
||||
link("GAMES", text_v_align: :center, font: FONT_BLACK, margin_left: PADDING) { page(Page::Games) }
|
||||
link("SERVERS", text_v_align: :center, font: FONT_BLACK, margin_left: PADDING) { page(Page::ServerBrowser) }
|
||||
flow(width: 1.0, fill: true) do
|
||||
link("GAMES",height: 1.0, text_v_align: :center, font: FONT_BLACK, margin_left: PADDING) { page(Page::Games) }
|
||||
link("SERVERS",height: 1.0, text_v_align: :center, font: FONT_BLACK, margin_left: PADDING) { page(Page::ServerBrowser) }
|
||||
stack(fill: true)
|
||||
image safe_get_image("#{ROOT_PATH}/media/icons/import.png"), height: 40, color: 0xff_bbbbbb, tip: "Downloads" do |img|
|
||||
dialog(Dialog::Downloads)
|
||||
@@ -40,31 +42,31 @@ module W3DHubLauncher
|
||||
image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 40, color: 0xff_bbbbbb, tip: "Notifications"
|
||||
end
|
||||
# application task status bar container
|
||||
stack(width: 1.0, fill: true, margin_left: PADDING) do
|
||||
@application_task_status_bar_container = stack(width: 1.0, fill: true, margin_left: PADDING, visible: false) do
|
||||
flow(width: 1.0) do
|
||||
para "Updating Red Alert: A Path Beyond (Release)"
|
||||
para "Updating Red Alert: A Path Beyond (Release)", tag: :status_bar_title
|
||||
stack(fill: true)
|
||||
para "Fetching manifests..."
|
||||
para "Fetching manifests...", tag: :status_bar_label
|
||||
end
|
||||
progress(width: 1.0, fraction: 0.75)
|
||||
progress(width: 1.0, fraction: 0.0, tag: :status_bar_progress)
|
||||
end
|
||||
end
|
||||
|
||||
# self account container
|
||||
flow(width: 300, height: 80, margin_left: LARGE_PADDING) do
|
||||
@account_container = flow(width: 400, height: 80, margin_left: LARGE_PADDING) do
|
||||
# self avatar container
|
||||
stack(width: 80, height: 1.0, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png"))) do |i|
|
||||
stack(width: 80, height: 1.0, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png")), tag: :account_avatar) do |i|
|
||||
i.subscribe(:clicked_left_mouse_button) do
|
||||
dialog(Dialog::Account)
|
||||
end
|
||||
# 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, tag: :account_online_state)
|
||||
end
|
||||
|
||||
stack(fill: true, height: 1.0, margin_left: HALF_PADDING) do
|
||||
flow(fill: true)
|
||||
# self name
|
||||
caption "cyberarm", font: FONT_BLACK, text_wrap: :none
|
||||
caption "cyberarm", font: FONT_BLACK, text_wrap: :none, width: 1.0, tag: :account_name
|
||||
# self set online state
|
||||
link "Online ▼", text_size: 18 do |l|
|
||||
menu(parent: l) do
|
||||
@@ -86,8 +88,12 @@ module W3DHubLauncher
|
||||
@page_host = stack(fill: true, height: 1.0) do
|
||||
end
|
||||
|
||||
# server details container
|
||||
@server_details_container = stack(width: 400, height: 1.0, margin_left: LARGE_PADDING, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||
end
|
||||
|
||||
# battleview/friends container
|
||||
@battleview_container = stack(width: 300, height: 1.0, margin_left: LARGE_PADDING, visible: true) do
|
||||
@battleview_container = stack(width: 400, height: 1.0, margin_left: LARGE_PADDING) do
|
||||
# friend management container
|
||||
flow(width: 1.0, height: 60) do
|
||||
flow(width: 1.0, v_align: :center) do
|
||||
@@ -101,25 +107,25 @@ module W3DHubLauncher
|
||||
stack(width: 1.0, fill: true, margin_top: LARGE_PADDING, scroll: true) do
|
||||
50.times do |i|
|
||||
# friend container
|
||||
widget(width: 1.0, height: 48, padding_top: HALF_PADDING, padding_bottom: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, hover: { background_nine_slice_color: ALPHA_GRAY }, active: { background_nine_slice_color: ALPHA_BLACK }) do |w|
|
||||
widget(width: 1.0, height: 68, padding_top: HALF_PADDING, padding_bottom: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, hover: { background_nine_slice_color: ALPHA_GRAY }, active: { background_nine_slice_color: ALPHA_BLACK }) do |w|
|
||||
w.subscribe(:clicked_left_mouse_button) do
|
||||
puts "HELLO THERE"
|
||||
end
|
||||
|
||||
|
||||
# friend avatar container
|
||||
stack(width: 48, height: 1.0, margin_left: HALF_PADDING, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png"))) do
|
||||
stack(width: 48 + HALF_PADDING, height: 1.0, margin_left: HALF_PADDING, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png")), background_image_mode: :fill) do
|
||||
stack(width: 12, height: 12, v_align: :bottom, h_align: :right, background_image: safe_get_image("#{ROOT_PATH}/media/ui/circle_small.png"), background_image_color: 0xff_26a269)
|
||||
end
|
||||
# friend name and status container
|
||||
stack(fill: true, height: 1.0, margin_left: HALF_PADDING, margin_right: HALF_PADDING) do
|
||||
stack(v_align: :center) do
|
||||
stack(width: 1.0, v_align: :center) do
|
||||
caption ["Silverlight", "PXD2000", "Alstar", "SteelGhost", "FRAYDO"].sample, text_wrap: :none
|
||||
inscription "RA_Under • 13:52", text_wrap: :none, margin_top: -HALF_PADDING
|
||||
end
|
||||
end
|
||||
# friend active application container
|
||||
stack(width: 48, height: 1.0, margin_right: HALF_PADDING, background_image: safe_get_image("#{ROOT_PATH}/media/logo.png"))
|
||||
stack(width: 48, height: 1.0, margin_right: HALF_PADDING, background_image: safe_get_image("#{ROOT_PATH}/media/logo.png"), background_image_mode: :fill)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -129,9 +135,226 @@ module W3DHubLauncher
|
||||
end
|
||||
end
|
||||
|
||||
hide_server_details_panel
|
||||
|
||||
page(Page::Games)
|
||||
end
|
||||
|
||||
def hide_battleview_panel
|
||||
@battleview_container.hide
|
||||
end
|
||||
|
||||
def show_battleview_panel
|
||||
@battleview_container.show
|
||||
end
|
||||
|
||||
def hide_server_details_panel
|
||||
@server_details_container.hide
|
||||
end
|
||||
|
||||
def show_server_details_panel
|
||||
@server_details_container.show
|
||||
end
|
||||
|
||||
def populate_server_information(server)
|
||||
return unless @server_details_container.visible?
|
||||
|
||||
# FIXME: check server channel against application channel's `server_channel`
|
||||
application = MemCache[:settings].applications.find { |app| app.id == server.game && app.channel == server.channel }
|
||||
# channel = MemCache[:applications].find { |appl| appl.id == application&.id }&.channels&.find { |c| c.id == application&.channel }
|
||||
|
||||
@server_details_container.clear do
|
||||
tagline server.name, width: 1.0, text_wrap: :none, tip: server.name
|
||||
map_image_container = stack(width: 1.0, height: (@server_details_container.content_width / 640.0) * 360, background_image: safe_get_image(server.map_preview_image_path, fallback_path: "#{ROOT_PATH}/media/default_map_preview.png")) do
|
||||
caption(
|
||||
server.current_map,
|
||||
text_border: false,
|
||||
text_border_size: 1,
|
||||
text_border_color: Gosu::Color::RED,
|
||||
text_shadow: false,
|
||||
width: 1.0,
|
||||
height: 1.0,
|
||||
padding_bottom: PADDING,
|
||||
text_align: :center,
|
||||
text_v_align: :bottom,
|
||||
tip: server.current_map
|
||||
)
|
||||
end
|
||||
unless File.exist?(server.map_preview_image_path)
|
||||
Worker::Api.server_map_image(server) do |result, status|
|
||||
# If the requested image is still the needed image?
|
||||
next unless @server_details_container.children.include?(map_image_container)
|
||||
next unless result.okay?
|
||||
|
||||
post_process_map_preview_image(result.data["image_path"])
|
||||
map_image_container.background_image = safe_get_image(result.data["image_path"])
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0, margin_top: PADDING) do
|
||||
button "JOIN SERVER", **CTA_BUTTON_THEME, width: 1.0, enabled: !application.nil?, tip: application.nil? ? "Application not installed" : "" do
|
||||
ApplicationHelper.join_server(application, server)
|
||||
end
|
||||
end
|
||||
|
||||
stack(width: 1.0, fill: true, scroll: true, margin_top: PADDING) do
|
||||
# misc. server details
|
||||
stack(width: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||
flow(width: 1.0) do
|
||||
stack(width: 1.0 / 3) do
|
||||
caption "Map", text_wrap: :none, tip: "Map", width: 1.0
|
||||
inscription server.current_map, text_wrap: :none, width: 1.0, tip: server.current_map
|
||||
end
|
||||
stack(width: 1.0 / 3, margin_left: HALF_PADDING) do
|
||||
display_ping = server.ping == Worker::Api::GameServer::BAD_OR_UNKNOWN_PING ? "?" : format("%dms", server.ping)
|
||||
caption "Ping", text_wrap: :none, tip: "Ping", width: 1.0
|
||||
inscription display_ping, text_wrap: :none, width: 1.0, tip: display_ping
|
||||
end
|
||||
stack(width: 1.0 / 3, margin_left: HALF_PADDING) do
|
||||
caption "Players", text_wrap: :none, tip: "Players", width: 1.0
|
||||
inscription format("%d/%d", server.player_count, server.max_players), text_wrap: :none, width: 1.0, tip: format("%d/%d", server.player_count, server.max_players)
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0) do
|
||||
stack(width: 1.0 / 3) do
|
||||
caption "Next Map", text_wrap: :none, tip: "Next Map", width: 1.0
|
||||
inscription server.next_map, text_wrap: :none, width: 1.0, tip: server.next_map
|
||||
end
|
||||
stack(width: 1.0 / 3, margin_left: HALF_PADDING) do
|
||||
caption "Time", text_wrap: :none, tip: "Time", width: 1.0
|
||||
inscription "00:00:00", text_wrap: :none, width: 1.0, tip: "00:00:00"
|
||||
end
|
||||
stack(width: 1.0 / 3, margin_left: HALF_PADDING) do
|
||||
caption "Time Left", text_wrap: :none, tip: "Time Left", width: 1.0
|
||||
inscription server.match_remaining_time, text_wrap: :none, width: 1.0, tip: server.match_remaining_time
|
||||
end
|
||||
end
|
||||
|
||||
flow(width: 1.0) do
|
||||
stack(width: 1.0 / 3) do
|
||||
caption "Region", text_wrap: :none, tip: "Region", width: 1.0
|
||||
inscription server.region, text_wrap: :none, width: 1.0, tip: server.region
|
||||
end
|
||||
stack(width: 1.0 / 3, margin_left: HALF_PADDING) do
|
||||
caption "Channel", text_wrap: :none, tip: "Channel", width: 1.0
|
||||
inscription server.channel, text_wrap: :none, width: 1.0, tip: server.channel
|
||||
end
|
||||
stack(width: 1.0 / 3, margin_left: HALF_PADDING) do
|
||||
caption "Version", text_wrap: :none, tip: "Version", width: 1.0
|
||||
inscription server.version, text_wrap: :none, width: 1.0, tip: server.version
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# team data
|
||||
server.teams.each do |team|
|
||||
# ignore empty, non-standard teams
|
||||
next if !team.id.between?(0, 1) && team.players.empty?
|
||||
|
||||
stack(width: 1.0, margin_top: PADDING, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||
tagline team.name, text_wrap: :none, tip: team.name
|
||||
|
||||
flow(width: 1.0) do
|
||||
stack(width: 1 / 4.0) do
|
||||
caption "Score", text_wrap: :none, tip: "Score"
|
||||
inscription team.score, text_wrap: :none, tip: team.score
|
||||
end
|
||||
stack(width: 1 / 4.0, margin_left: HALF_PADDING) do
|
||||
caption "Kills", text_wrap: :none, tip: "Kills"
|
||||
inscription team.kills, text_wrap: :none, tip: team.kills
|
||||
end
|
||||
stack(width: 1 / 4.0, margin_left: HALF_PADDING) do
|
||||
caption "Deaths", text_wrap: :none, tip: "Deaths"
|
||||
inscription team.deaths, text_wrap: :none, tip: team.deaths
|
||||
end
|
||||
stack(width: 1 / 4.0, margin_left: HALF_PADDING) do
|
||||
caption "Players", text_wrap: :none, tip: "Players"
|
||||
inscription format("%d/%d", team.players.size, server.max_players), text_wrap: :none, tip: format("%d/%d", team.players.size, server.max_players)
|
||||
end
|
||||
end
|
||||
|
||||
next if team.players.empty?
|
||||
|
||||
flow(width: 1.0, height: 2, background: 0xaa_000000)
|
||||
|
||||
flow(width: 1.0) do
|
||||
stack(fill: true, margin_right: HALF_PADDING) do
|
||||
inscription "Name", width: 1.0, text_wrap: :none, font: FONT_BOLD, tip: "Name"
|
||||
end
|
||||
stack(width: 1 / 5.0, margin_right: HALF_PADDING) do
|
||||
inscription "Score", width: 1.0, text_wrap: :none, font: FONT_BOLD, tip: "Score"
|
||||
end
|
||||
stack(width: 1 / 5.0, margin_right: HALF_PADDING) do
|
||||
inscription "Kills", width: 1.0, text_wrap: :none, font: FONT_BOLD, tip: "Kills"
|
||||
end
|
||||
stack(width: 1 / 5.0, margin_right: HALF_PADDING) do
|
||||
inscription "Deaths", width: 1.0, text_wrap: :none, font: FONT_BOLD, tip: "Deaths"
|
||||
end
|
||||
# stack(width: 1 / 8.0, margin_right: HALF_PADDING) do
|
||||
# inscription "Ping", width: 1.0, text_wrap: :none, font: FONT_BOLD, tip: "Ping"
|
||||
# end
|
||||
# stack(width: 1 / 6.0) do
|
||||
# inscription "Time", width: 1.0, text_wrap: :none, font: FONT_BOLD, tip: "Time"
|
||||
# end
|
||||
end
|
||||
|
||||
team.players.sort_by(&:score).reverse.each_with_index do |player, i|
|
||||
flow(width: 1.0, background: i.odd? ? 0 : 0xaa_000000) do
|
||||
stack(fill: true, margin_right: HALF_PADDING) do
|
||||
inscription player.name, font: FONT_MONO, width: 1.0, text_wrap: :none, tip: player.name
|
||||
end
|
||||
stack(width: 1 / 5.0, margin_right: HALF_PADDING) do
|
||||
inscription player.score, font: FONT_MONO, width: 1.0, text_wrap: :none, tip: player.score
|
||||
end
|
||||
stack(width: 1 / 5.0, margin_right: HALF_PADDING) do
|
||||
inscription player.kills, font: FONT_MONO, width: 1.0, text_wrap: :none, tip: player.kills
|
||||
end
|
||||
stack(width: 1 / 5.0, margin_right: HALF_PADDING) do
|
||||
inscription player.deaths, font: FONT_MONO, width: 1.0, text_wrap: :none, tip: player.deaths
|
||||
end
|
||||
# stack(width: 1 / 8.0, margin_right: HALF_PADDING) do
|
||||
# inscription player.ping, font: FONT_MONO, width: 1.0, text_wrap: :none, tip: player.ping
|
||||
# end
|
||||
# stack(width: 1 / 5.0) do
|
||||
# inscription player.time, font: FONT_MONO, width: 1.0, text_wrap: :none, tip: player.time
|
||||
# end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get them rounded corners!
|
||||
def post_process_map_preview_image(path)
|
||||
image = Gosu::Image.new(path)
|
||||
|
||||
target_width = 640
|
||||
target_height= 360
|
||||
|
||||
nine_slice = CyberarmEngine::BackgroundNineSlice.new(
|
||||
image_path: NINE_SLICE_ROUNDED,
|
||||
mode: :stretch,
|
||||
width: target_width,
|
||||
height: target_height,
|
||||
left: NINE_SLICE_EDGE,
|
||||
right: NINE_SLICE_EDGE,
|
||||
top: NINE_SLICE_EDGE,
|
||||
bottom: NINE_SLICE_EDGE,
|
||||
render_mode: :multiply
|
||||
)
|
||||
|
||||
composite = Gosu.render(target_width, target_height) do
|
||||
image_scale = [target_width / image.width.to_f, target_height / image.height.to_f].max
|
||||
image.draw_rot(target_width / 2, target_height / 2, 0, 0, 0.5, 0.5, image_scale, image_scale)
|
||||
nine_slice.draw
|
||||
end
|
||||
|
||||
composite.save(path)
|
||||
end
|
||||
|
||||
def button_up(id)
|
||||
super
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ module W3DHubLauncher
|
||||
|
||||
Response = Data.define(:status, :request_id, :result)
|
||||
|
||||
attr_reader :w3dhub_api, :settings
|
||||
|
||||
def initialize
|
||||
end
|
||||
|
||||
@@ -29,10 +31,11 @@ module W3DHubLauncher
|
||||
def init_server
|
||||
@threads = []
|
||||
@requests = []
|
||||
@tasks = []
|
||||
|
||||
@client = nil
|
||||
|
||||
@settings = 0# Settings.new
|
||||
@settings = nil
|
||||
@game_servers = []
|
||||
@host_pings = {}
|
||||
|
||||
@@ -137,6 +140,15 @@ module W3DHubLauncher
|
||||
@client&.flush
|
||||
end
|
||||
|
||||
def message_requester(request_id:, status:, data:, error: nil)
|
||||
payload = { status: status, request_id: request_id, data: data, error: error }.to_json
|
||||
|
||||
@client&.write(payload)
|
||||
@client&.write(RESPONSE_SEPARATOR)
|
||||
@client&.flush
|
||||
end
|
||||
|
||||
|
||||
# Send request to server
|
||||
def request(query)
|
||||
# pp [:client_request, query]
|
||||
@@ -289,6 +301,66 @@ module W3DHubLauncher
|
||||
deliver_response(result, query)
|
||||
end
|
||||
|
||||
def server_map_image(query)
|
||||
result = CyberarmEngine::Result.new
|
||||
|
||||
# TODO: Ideally battleview would host readily accessible map previews
|
||||
server = @game_servers.find { |s| s.address == query.data["server"]["address"] && s.port == query.data["server"]["port"] }
|
||||
return deliver_response(result, query) unless server
|
||||
|
||||
# is application installed to source data from?
|
||||
application = @settings.application_installed?(server.game, server.channel)
|
||||
return deliver_response(result, query) unless application
|
||||
|
||||
save_path = server.map_preview_image_path
|
||||
if File.exist?(save_path)
|
||||
result.data = { image_path: save_path }
|
||||
|
||||
return deliver_response(result, query)
|
||||
end
|
||||
|
||||
map_mix = Dir.glob("#{application.installation_path}/**/#{query.data["server"]["map"]}")&.first
|
||||
return deliver_response(result, query) unless map_mix
|
||||
|
||||
mix = W3DHubLauncher::WWMix.new(path: map_mix)
|
||||
return deliver_response(result, query) unless mix.load
|
||||
|
||||
entry = nil
|
||||
canvas = nil
|
||||
|
||||
entry = mix.entries.find { |e| e.name.match?(/_map_preview\.png/i) }
|
||||
entry ||= mix.entries.find { |e| e.name.match?(/_map_preview\.dds/i) }
|
||||
entry ||= mix.entries.find { |e| e.name.match?(/screenshot\.png/i) }
|
||||
entry ||= mix.entries.find { |e| e.name.match?(/screenshot\.dds/i) }
|
||||
entry ||= mix.entries.find { |e| e.name.match?(/_map\.png/i) }
|
||||
entry ||= mix.entries.find { |e| e.name.match?(/_map\.dds/i) }
|
||||
|
||||
return deliver_response(result, query) unless entry
|
||||
return deliver_response(result, query) unless entry.read
|
||||
|
||||
if entry.name.match?(/\.dds/i)
|
||||
begin
|
||||
dds = W3DHubLauncher::DDS.new(io: StringIO.new(entry.blob))
|
||||
image_data = dds.images.first
|
||||
|
||||
canvas = ChunkyPNG::Canvas.from_rgba_stream(image_data.width, image_data.height, image_data.data)
|
||||
canvas.save(save_path)
|
||||
rescue StandardError => e
|
||||
result.error = e
|
||||
|
||||
return deliver_response(result, query)
|
||||
end
|
||||
|
||||
elsif entry.name.match?(/\.png/i)
|
||||
# canvas = ChunkyPNG::Canvas.from_blob(entry.blob)
|
||||
File.binwrite(save_path, entry.blob)
|
||||
end
|
||||
|
||||
result.data = { image_path: save_path }
|
||||
|
||||
deliver_response(result, query)
|
||||
end
|
||||
|
||||
def dns_resolution(query)
|
||||
result = CyberarmEngine::Result.new
|
||||
|
||||
@@ -322,6 +394,7 @@ module W3DHubLauncher
|
||||
begin
|
||||
if File.exist?(path) && File.size(path).positive?
|
||||
json = File.read(path)
|
||||
@settings = Api::Settings.new(JSON.parse(json))
|
||||
result.data = json
|
||||
else
|
||||
result.error = RuntimeError.new("Launcher settings file does not exist or is empty.")
|
||||
@@ -369,19 +442,32 @@ module W3DHubLauncher
|
||||
deliver_response(response.result, query)
|
||||
end
|
||||
|
||||
def install_application(query)
|
||||
def task_install_application(query)
|
||||
application = @w3dhub_api.applications.find { |app| app.id == query.data["app_id"] }
|
||||
channel = application.channels.find { |chan| chan.id == query.data["channel_id"] }
|
||||
|
||||
@tasks << Task::InstallApplication.new(
|
||||
request_id: query.request_id,
|
||||
application: application,
|
||||
channel: channel,
|
||||
installed_version: nil,
|
||||
target_version: channel.version
|
||||
)
|
||||
@tasks.last.start(self)
|
||||
|
||||
Response.new(Request::STATUS_PENDING, query.request_id, CyberarmEngine::Result.new(data: true))
|
||||
end
|
||||
|
||||
def update_application(query)
|
||||
def task_update_application(query)
|
||||
end
|
||||
|
||||
def repair_application(query)
|
||||
def task_repair_application(query)
|
||||
end
|
||||
|
||||
def move_application(query)
|
||||
def task_move_application(query)
|
||||
end
|
||||
|
||||
def uninstall_application(query)
|
||||
def task_uninstall_application(query)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,6 +16,10 @@ module W3DHubLauncher
|
||||
Worker::Request.new(:ico_to_png, { ico_path: ico_path, png_path: png_path }, &block)
|
||||
end
|
||||
|
||||
def self.server_map_image(server, &block)
|
||||
Worker::Request.new(:server_map_image, { server: { address: server.address, port: server.port, map: server.current_map } }, &block)
|
||||
end
|
||||
|
||||
def self.dns_resolution(&block)
|
||||
Worker::Request.new(:dns_resolution, "", &block)
|
||||
end
|
||||
@@ -54,42 +58,47 @@ module W3DHubLauncher
|
||||
|
||||
# returns news for application
|
||||
def self.news(category = "launcher-home", &block)
|
||||
Worker::Request.new(:news, category, &block)
|
||||
W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_news, arguments: [category] }, &block)
|
||||
end
|
||||
|
||||
# returns news for application
|
||||
def self.events(app_id, &block)
|
||||
Worker::Request.new(:events, app_id, &block)
|
||||
def self.events(category, &block)
|
||||
W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_events, arguments: [category] }, &block)
|
||||
end
|
||||
|
||||
# request installation of application
|
||||
#
|
||||
# periodically reports progress until completion
|
||||
def self.install_application(app_id, channel_id)
|
||||
def self.install_application(app_id, channel_id, &block)
|
||||
Worker::Request.new(:task_install_application, { app_id: app_id, channel_id: channel_id }, &block)
|
||||
end
|
||||
|
||||
# request update of application
|
||||
#
|
||||
# periodically reports progress until completion
|
||||
def self.update_application(app_id, channel_id)
|
||||
def self.update_application(app_id, channel_id, &block)
|
||||
Worker::Request.new(:task_update_application, { app_id: app_id, channel_id: channel_id }, &block)
|
||||
end
|
||||
|
||||
# request repair of application
|
||||
#
|
||||
# periodically reports progress until completion
|
||||
def self.repair_application(app_id, channel_id)
|
||||
def self.repair_application(app_id, channel_id, &block)
|
||||
Worker::Request.new(:task_repair_application, { app_id: app_id, channel_id: channel_id }, &block)
|
||||
end
|
||||
|
||||
# request relocation of application
|
||||
#
|
||||
# periodically reports progress until completion
|
||||
def self.move_application(app_id, channel_id)
|
||||
def self.move_application(app_id, channel_id, target_directory, &block)
|
||||
Worker::Request.new(:task_move_application, { app_id: app_id, channel_id: channel_id, target_directory: target_directory }, &block)
|
||||
end
|
||||
|
||||
# request removal of application
|
||||
#
|
||||
# periodically reports progress until completion
|
||||
def self.uninstall_application(app_id, channel_id)
|
||||
def self.uninstall_application(app_id, channel_id, &block)
|
||||
Worker::Request.new(:task_uninstall_application, { app_id: app_id, channel_id: channel_id }, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ module W3DHubLauncher
|
||||
def extended_data(key, default)
|
||||
v = @extended_data&.find { |d| d.name == key }&.value
|
||||
if v.nil?
|
||||
v = @_app.extended_data(key, value)
|
||||
v = @_app.extended_data(key, default)
|
||||
|
||||
return default if v.nil?
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@ module W3DHubLauncher
|
||||
# Designed to work with cyberarm's Game Server Hub service data, may work with W3D Hub's service with degraded metadata.
|
||||
class GameServer
|
||||
BAD_OR_UNKNOWN_PING = 1_000_000
|
||||
NO_OR_DEFAULT_VERSION = "838".freeze
|
||||
|
||||
attr_reader :id, :game, :channel, :address, :port, :region,
|
||||
:name, :password, :current_map, :player_count, :max_players,
|
||||
@@ -38,15 +39,17 @@ module W3DHubLauncher
|
||||
|
||||
# status player data
|
||||
data["status"]["players"].each do |player_data|
|
||||
team = @teams.find { |t| t.id == player_data["id"] }
|
||||
team = @teams.find { |t| t.id == player_data["team"] }
|
||||
next unless team
|
||||
|
||||
team.players << Player.new(player_data)
|
||||
end
|
||||
|
||||
# Cyberarm GSH extras
|
||||
@version = data["version"] || "838"
|
||||
@version = data["version"] || NO_OR_DEFAULT_VERSION
|
||||
@next_map = data["status"]["nextmap"] || ""
|
||||
|
||||
@version = Gem::Version.new(@version) if @version.split(".").size == 4
|
||||
end
|
||||
|
||||
# TODO: implement :)
|
||||
@@ -86,6 +89,10 @@ module W3DHubLauncher
|
||||
0xff_3d3846
|
||||
end
|
||||
end
|
||||
|
||||
def map_preview_image_path
|
||||
format("%s/map_preview_%s.png", W3DHubLauncher::CACHE_PATH, Digest::SHA256.hexdigest("#{@game}_#{@channel}_#{@current_map}"))
|
||||
end
|
||||
end
|
||||
|
||||
class Team
|
||||
|
||||
95
lib/worker/api/legacy_manifest.rb
Normal file
95
lib/worker/api/legacy_manifest.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
module W3DHubLauncher
|
||||
class Worker
|
||||
class Api
|
||||
class LegacyManifest
|
||||
attr_reader :game, :type, :version, :base_version, :files, :dependencies
|
||||
|
||||
def initialize(file_path)
|
||||
@document = REXML::Document.new(File.read(file_path))
|
||||
root = @document.root
|
||||
|
||||
@game = root["game"]
|
||||
@type = root["type"]
|
||||
@version = root["version"] ? Gem::Version.new(root["version"]) : nil
|
||||
@base_version = root["baseVersion"] ? Gem::Version.new(root["baseVersion"]) : nil
|
||||
|
||||
@files = []
|
||||
@dependencies = []
|
||||
|
||||
parse_files
|
||||
parse_dependencies
|
||||
end
|
||||
|
||||
def patch?
|
||||
@type == "Patch"
|
||||
end
|
||||
|
||||
def full?
|
||||
@type == "Full"
|
||||
end
|
||||
|
||||
def parse_files
|
||||
@document.root.elements.each("//File") do |element|
|
||||
@files.push(ManifestFile.new(element, @version))
|
||||
end
|
||||
end
|
||||
|
||||
def parse_dependencies
|
||||
@document.root.elements.each("//Dependency") do |element|
|
||||
@dependencies.push(Dependency.new(element))
|
||||
end
|
||||
end
|
||||
|
||||
class ManifestFile
|
||||
attr_reader :name, :checksum, :package, :patch_package, :removed_since, :from, :version
|
||||
|
||||
def initialize(xml, version)
|
||||
@data = xml
|
||||
|
||||
@name = @data["name"]
|
||||
@checksum = @data["checksum"]
|
||||
@package = @data["package"]
|
||||
@removed_since = @data["removedsince"] ? Gem::Version.new(@data["removedsince"]) : nil
|
||||
|
||||
xml.elements.each("Patch") do |patch|
|
||||
@patch = true
|
||||
|
||||
@from = patch["from"]
|
||||
|
||||
if @package.to_s.empty?
|
||||
@package = patch["package"]
|
||||
else
|
||||
@patch_package = patch["package"]
|
||||
end
|
||||
end
|
||||
|
||||
@version = version ? Gem::Version.new(version) : nil
|
||||
end
|
||||
|
||||
def removed?
|
||||
@removed_since
|
||||
end
|
||||
|
||||
def patch?
|
||||
@patch
|
||||
end
|
||||
|
||||
def full_and_patch?
|
||||
@package && @patch_package
|
||||
end
|
||||
end
|
||||
|
||||
class Dependency
|
||||
attr_reader :name, :removed_since
|
||||
|
||||
def initialize(xml)
|
||||
@data = xml
|
||||
|
||||
@name = @data["name"]
|
||||
@removed_since = @data["removed_since"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
72
lib/worker/api/legacy_manifest_package.rb
Normal file
72
lib/worker/api/legacy_manifest_package.rb
Normal file
@@ -0,0 +1,72 @@
|
||||
require "json"
|
||||
require "digest/sha2"
|
||||
|
||||
module W3DHubLauncher
|
||||
class Worker
|
||||
class Api
|
||||
class LegacyManifestPackage
|
||||
attr_reader :category, :subcategory, :version, :name, :error, :file_size,
|
||||
:checksum_chunk_size, :sha256_checksum, :checksum_chunks, :download_url
|
||||
|
||||
def initialize(hash)
|
||||
@category = hash["category"]
|
||||
@subcategory = hash["subcategory"]
|
||||
@version = Gem::Version.new(hash["version"] || "0.0.0.0")
|
||||
@name = hash["name"]
|
||||
@error = hash["error"]
|
||||
@file_size = Integer(hash["size"] || 0)
|
||||
@checksum_chunk_size = Integer(hash["checksum-chunk-size"] || 0)
|
||||
@sha256_checksum = hash["checksum"]
|
||||
@checksum_chunks = hash["checksum-chunks"]
|
||||
@download_url = hash["download_url"]
|
||||
end
|
||||
|
||||
def error?
|
||||
@error
|
||||
end
|
||||
|
||||
def version?
|
||||
@version != Gem::Version.new("0.0.0.0")
|
||||
end
|
||||
|
||||
# checksum whole file and file chunks until a mismatch occurs or the whole file is verified.
|
||||
def verify_file(filename)
|
||||
return false unless File.exist?(filename) && !File.directory?(filename)
|
||||
|
||||
file_size = File.size(filename)
|
||||
|
||||
overall_digest = Digest::SHA256.new
|
||||
chunk_digest = Digest::SHA256.new
|
||||
|
||||
File.open(filename, "rb") do |f|
|
||||
f.pos = 0
|
||||
offset = 0
|
||||
last_valid_offset = 0
|
||||
|
||||
while (chunk = f.read(@checksum_chunk_size))
|
||||
overall_digest << chunk
|
||||
|
||||
# return last valid chunk on invalid chunk
|
||||
return last_valid_offset unless @checksum_chunks[offset.to_s] == chunk_digest.update(chunk).hexdigest.upcase
|
||||
|
||||
last_valid_offset = offset
|
||||
offset += @checksum_chunk_size
|
||||
chunk_digest.reset
|
||||
end
|
||||
end
|
||||
|
||||
# return boolean after completely digesting file
|
||||
@sha256_checksum == overall_digest.hexdigest.upcase
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def package_cache_filename(category, subcategory)
|
||||
if subcategory.empty?
|
||||
category
|
||||
else
|
||||
"#{category}-#{subcategory}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
lib/worker/api/news_item.rb
Normal file
28
lib/worker/api/news_item.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module W3DHubLauncher
|
||||
class Worker
|
||||
class Api
|
||||
class NewsItem
|
||||
attr_reader :topic_id, :title, :blurb, :uri, :image_uri, :author_id, :author, :author_uri, :timestamp
|
||||
|
||||
def initialize(data)
|
||||
@topic_id = Integer(data["topic-id"] || 0)
|
||||
@title = data["title"].to_s
|
||||
@blurb = clean_and_scrub_blurb(data["blurb"].to_s)
|
||||
@uri = data["uri"].to_s
|
||||
@image_uri = data["image"].to_s
|
||||
|
||||
@author_id = data["author-id"].to_s
|
||||
@author = data["author"].to_s
|
||||
@author_uri = data["author-uri"].to_s
|
||||
|
||||
@timestamp = Time.at(Integer(data["timestamp"] || 0), in: "UTC").localtime
|
||||
end
|
||||
|
||||
# Replaces multiple newlines with a single newline, and trims off any leading and trailing whitespace.
|
||||
def clean_and_scrub_blurb(string)
|
||||
string.gsub(/\n+/, "\n").strip
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
lib/worker/api/server_event.rb
Normal file
17
lib/worker/api/server_event.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
module W3DHubLauncher
|
||||
class Worker
|
||||
class Api
|
||||
class ServerEvent
|
||||
attr_reader :title, :image_uri, :app_id, :start_time, :end_time, :timestamp
|
||||
def initialize(data)
|
||||
@title = data["title"]
|
||||
@image_uri = data["image"]
|
||||
@app_id = data["server"]
|
||||
@start_time = Time.parse(data["starttime"], in: "UTC").localtime
|
||||
@end_time = Time.parse(data["endtime"], in: "UTC").localtime
|
||||
@timestamp = Time.parse(data["dateTime"], in: "UTC").localtime
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,13 @@ module W3DHubLauncher
|
||||
}.to_json(options)
|
||||
end
|
||||
|
||||
def application_installed?(application, channel)
|
||||
app_id = application.is_a?(String) ? application : application.id
|
||||
channel_id = channel.is_a?(String) ? channel : channel.id
|
||||
|
||||
applications.find { |app| app.id == app_id && app.channel == channel_id }
|
||||
end
|
||||
|
||||
# User explictly set options
|
||||
class Preferences
|
||||
SCHEMA = 0
|
||||
@@ -74,17 +81,18 @@ module W3DHubLauncher
|
||||
SCHEMA = 0
|
||||
|
||||
attr_reader :id, :channel
|
||||
attr_accessor :version, :installation_path, :wine_prefix_path, :launch_command
|
||||
attr_accessor :version, :installation_path, :wine_prefix_path, :launch_command, :timestamp
|
||||
|
||||
def self.create(id:, channel:, version:, installation_path:, wine_prefix_path:, launch_command:)
|
||||
def self.create(id:, channel:, version:, installation_path:, wine_prefix_path:, launch_command:, timestamp:)
|
||||
hash = {
|
||||
"schema" => SCHEMA,
|
||||
"id" => id,
|
||||
"channel" => channel,
|
||||
"version" => Gem::Version.new(version),
|
||||
"version" => version,
|
||||
"installation_path" => installation_path,
|
||||
"wine_prefix_path" => wine_prefix_path,
|
||||
"launch_command" => launch_command,
|
||||
"timestamp" => timestamp
|
||||
}
|
||||
|
||||
self.new(hash)
|
||||
@@ -99,6 +107,7 @@ module W3DHubLauncher
|
||||
@installation_path = data["installation_path"]
|
||||
@wine_prefix_path = data["wine_prefix_path"]
|
||||
@launch_command = data["launch_command"]
|
||||
@timestamp = Time.at(data["timestamp"])
|
||||
end
|
||||
|
||||
def to_json(options = {})
|
||||
@@ -109,7 +118,8 @@ module W3DHubLauncher
|
||||
version: @version.to_s,
|
||||
installation_path: @installation_path,
|
||||
wine_prefix_path: @wine_prefix_path,
|
||||
launch_command: @launch_command
|
||||
launch_command: @launch_command,
|
||||
timestamp: @timestamp.to_i
|
||||
}.to_json(options)
|
||||
end
|
||||
end
|
||||
|
||||
19
lib/worker/api/test_event.rb
Normal file
19
lib/worker/api/test_event.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module W3DHubLauncher
|
||||
class Worker
|
||||
class Api
|
||||
class TestEvent
|
||||
attr_reader :title, :image_uri, :timestamp
|
||||
|
||||
def initialize(data)
|
||||
@title = data["name"]
|
||||
# @title = data["title"]
|
||||
@image_uri = data["image"]
|
||||
# @app_id = data["server"]
|
||||
# @start_time = Time.parse(data["starttime"]).localtime
|
||||
# @end_time = Time.parse(data["endtime"]).localtime
|
||||
@timestamp = Time.parse(data["dateTime"], in: "UTC").localtime
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,12 +3,6 @@ module W3DHubLauncher
|
||||
class Request
|
||||
Query = Data.define(:type, :request_id, :data)
|
||||
|
||||
FETCH_URL = 0
|
||||
DOWNLOAD_URL = 1
|
||||
W3DHUB_API_CALL = 10
|
||||
LAUNCHER_SETTINGS = 1000
|
||||
LAUNCHER_UPDATE_SETTINGS = 1001
|
||||
|
||||
STATUS_ERROR = -1 # request has failed
|
||||
STATUS_PENDING = 0 # request has not yet started
|
||||
STATUS_OK = 1 # request completed successfully
|
||||
@@ -54,9 +48,10 @@ module W3DHubLauncher
|
||||
def handle_event(event, data)
|
||||
# pp [event, data]
|
||||
|
||||
@callback&.call(data, event)
|
||||
|
||||
case event
|
||||
when STATUS_ERROR, STATUS_COMPLETE
|
||||
@callback&.call(data)
|
||||
Request.requests.delete(self)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,561 @@
|
||||
module W3DHubLauncher
|
||||
class Task
|
||||
# ignore *.meta and paths.ini files
|
||||
IGNORED_FILES = [/\A.+\.meta\z/i, /\A.+\/paths\.ini\z/i].freeze
|
||||
|
||||
IndexedFile = Data.define(:type, :path)
|
||||
|
||||
attr_reader :request_id, :application, :channel, :installed_version, :target_version
|
||||
|
||||
def initialize(request_id:, application:, channel:, installed_version:, target_version:)
|
||||
@request_id = request_id
|
||||
@application = application
|
||||
@channel = channel
|
||||
@installed_version = installed_version
|
||||
@target_version = target_version
|
||||
|
||||
pp self
|
||||
|
||||
# caching mechinism for file path normalization
|
||||
@file_index = {}
|
||||
# manifests!
|
||||
@manifests = {}
|
||||
# files that must exist for a valid installation
|
||||
@manifest_files = []
|
||||
# files that should be removed
|
||||
@deleted_manifest_files = []
|
||||
# files needed to make installation valid for target version
|
||||
@required_manifest_files = []
|
||||
# packages to verify and/or download
|
||||
@packages = []
|
||||
|
||||
setup
|
||||
end
|
||||
|
||||
def setup
|
||||
end
|
||||
|
||||
def start(worker)
|
||||
@worker = worker
|
||||
|
||||
@installation_directory ||= application_target_installation_directory
|
||||
|
||||
update_status(label: "Starting...", fraction: 0.0)
|
||||
|
||||
execute
|
||||
end
|
||||
|
||||
def abort_task!(reason = "")
|
||||
puts reason
|
||||
|
||||
message_requester(Worker::Request::STATUS_ERROR, data: nil, error: reason)
|
||||
|
||||
raise reason
|
||||
end
|
||||
|
||||
# high level methods
|
||||
|
||||
def index_installation_directory
|
||||
return unless File.directory?(@installation_directory)
|
||||
|
||||
Dir.glob("#{@installation_directory}/**/**").each do |path|
|
||||
add_to_file_index(path)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_manifests(version = @target_version)
|
||||
update_status(label: "Fetching manifests...", fraction: 0.0)
|
||||
|
||||
while (manifest = fetch_manifest(version))
|
||||
@manifests[version] = manifest
|
||||
|
||||
version = manifest.base_version
|
||||
break unless version
|
||||
break if manifest.full?
|
||||
end
|
||||
end
|
||||
|
||||
def build_package_list
|
||||
update_status(label: "Building package list...", fraction: 0.0)
|
||||
|
||||
channel_manifests = []
|
||||
version = @target_version
|
||||
|
||||
while(manifest = @manifests[version])
|
||||
channel_manifests << manifest
|
||||
|
||||
break if manifest.full?
|
||||
|
||||
version = manifest.base_version
|
||||
end
|
||||
|
||||
files = []
|
||||
deleted_files = []
|
||||
channel_manifests.reverse.each do |manifest|
|
||||
manifest.files.each do |file|
|
||||
if file.removed?
|
||||
files.delete_if { |f| f.name.casecmp?(file.name) }
|
||||
deleted_files << file
|
||||
|
||||
next
|
||||
end
|
||||
|
||||
# remove files that are total replacements, not patches
|
||||
files.delete_if { |f| f.name.casecmp?(file.name) } unless file.patch?
|
||||
|
||||
# exclude ignored files from consideration
|
||||
files.delete_if { |f| ignored_file?(f.name) }
|
||||
|
||||
# add file to file list
|
||||
files << file
|
||||
end
|
||||
end
|
||||
|
||||
@manifest_files = files
|
||||
@deleted_manifest_files = deleted_files
|
||||
end
|
||||
|
||||
def verify_files
|
||||
update_status(label: "Verifying files...", fraction: 0.0)
|
||||
|
||||
@required_manifest_files = @manifest_files.clone
|
||||
|
||||
# Process manifest game files in NEWEST to OLDEST order so that we don't erroneously flag
|
||||
# valid files as invalid due to an OLDER version of the file being checked FIRST.
|
||||
@manifest_files.reverse.each do |file|
|
||||
break unless File.directory?(@installation_directory)
|
||||
|
||||
# skip irrelevant older versions of files
|
||||
next unless @required_manifest_files.include?(file)
|
||||
|
||||
file_path = normalize_path(file.name)
|
||||
|
||||
next unless File.exist?(file_path) # && !File.directory?(file_path)
|
||||
|
||||
puts "verifying #{file_path}..."
|
||||
checksum = Digest::SHA256.file(file_path).hexdigest.upcase
|
||||
|
||||
if checksum == file.checksum
|
||||
@required_manifest_files.delete(file)
|
||||
|
||||
# remove irrelevant older versions of the file from consideration
|
||||
@required_manifest_files.delete_if { |f| f.name.casecmp?(file.name) && f.version < file.version }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_packages
|
||||
update_status(label: "Downloading packages...", fraction: 0.0)
|
||||
|
||||
required_packages = {}
|
||||
@required_manifest_files.each do |file|
|
||||
required_packages[file.version] ||= []
|
||||
|
||||
next if required_packages[file.version].find { |pkg| pkg.casecmp?(file.package) }
|
||||
|
||||
required_packages[file.version] << file.package
|
||||
end
|
||||
|
||||
packages = required_packages.map do |version, packages|
|
||||
packages.map do |package_name|
|
||||
{
|
||||
category: @application.category,
|
||||
subcategory: @application.id,
|
||||
name: format("%s.zip", package_name),
|
||||
version: version.to_s
|
||||
}
|
||||
end
|
||||
end.flatten
|
||||
|
||||
return if packages.empty?
|
||||
|
||||
result = @worker.w3dhub_api.fetch_package_details(packages)
|
||||
|
||||
unless result.okay?
|
||||
abort_task!("Failed to fetch required package details!")
|
||||
end
|
||||
|
||||
response = JSON.parse(result.data)
|
||||
manifest_packages = response["packages"]&.map { |item| Worker::Api::LegacyManifestPackage.new(item) }
|
||||
if (failed_packages = manifest_packages.select(&:error?)) && !failed_packages.empty?
|
||||
abort_task!("Failed to retrieve packages details for: #{failed_packages.map { |pkg| "#{pkg.name}:#{pkg.version}: #{pkg.error}"}.join(', ') }")
|
||||
end
|
||||
|
||||
@packages = manifest_packages
|
||||
|
||||
manifest_packages.each do |pkg|
|
||||
file_path = package_cache_path(pkg)
|
||||
unless File.directory?(File.dirname(file_path))
|
||||
puts "creating directory: #{File.dirname(file_path)}"
|
||||
create_directory(File.dirname(file_path))
|
||||
end
|
||||
|
||||
partially_valid_at = 0
|
||||
state = pkg.verify_file(file_path)
|
||||
if state.is_a?(Integer)
|
||||
puts "partially valid at: #{state} bytes (#{file_path})" # 20971520
|
||||
partially_valid_at = state
|
||||
else
|
||||
if state == true # completely verified, skip download!
|
||||
puts "skipping #{file_path}"
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
result = if pkg.download_url
|
||||
puts "downloading #{pkg.download_url} to #{file_path}"
|
||||
@worker.w3dhub_api.download(pkg.download_url, path: file_path, headers: @worker.w3dhub_api.headers(range: partially_valid_at))
|
||||
else
|
||||
# TODO xD
|
||||
@worker.w3dhub_api.fetch_package("TODO")
|
||||
end
|
||||
|
||||
abort_task!("Failed to download required package: #{pkg.name}:#{pkg.version} (#{result.error})") unless result.okay?
|
||||
end
|
||||
end
|
||||
|
||||
def install_packages
|
||||
update_status(label: "Installing...", fraction: 0.0)
|
||||
|
||||
create_directory(@installation_directory)
|
||||
|
||||
processed_packages = {}
|
||||
@required_manifest_files.each do |manifest_file|
|
||||
package = @packages.find do |pkg|
|
||||
pkg.name.casecmp?("#{manifest_file.package}.zip") && manifest_file.version == pkg.version
|
||||
end
|
||||
package_path = package_cache_path(package)
|
||||
|
||||
next if processed_packages[package_path]
|
||||
|
||||
if manifest_file.patch?
|
||||
apply_patch(manifest_file, package)
|
||||
else
|
||||
unzip(package_path)
|
||||
end
|
||||
|
||||
processed_packages[package_path] = manifest_file
|
||||
end
|
||||
end
|
||||
|
||||
def remove_deleted_files
|
||||
update_status(label: "Removing files...", fraction: 0.0)
|
||||
|
||||
@deleted_manifest_files.each do |manifest_file|
|
||||
file_path = normalize_path(manifest_file.name)
|
||||
|
||||
if File.exist?(file_path) && !File.directory?(file_path)
|
||||
puts "Removing file: #{file_path}"
|
||||
File.delete(file_path)
|
||||
|
||||
remove_from_file_index(file_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_paths_ini
|
||||
File.open(normalize_path("data/paths.ini"), "w") do |file|
|
||||
file.puts("[paths]")
|
||||
file.puts("RegBase=W3D Hub")
|
||||
file.puts("RegClient=#{@application.category}\\#{@application.id}-#{@channel.id}")
|
||||
file.puts("RegFDS=#{@application.category}\\#{@application.id}-#{@channel.id}-server")
|
||||
file.puts("FileBase=W3D Hub");
|
||||
file.puts("FileClient=#{@application.category}\\#{@application.id}-#{@channel.id}")
|
||||
file.puts("FileFDS=#{@application.category}\\#{@application.id}-#{@channel.id}-server")
|
||||
|
||||
file.puts("UseRenFolder=#{@channel.extended_data("usesRenFolder", false)}")
|
||||
end
|
||||
end
|
||||
|
||||
# updated, and moved applications will overwrite existing application data in settings
|
||||
def mark_application_installed
|
||||
app = Worker::Api::Settings::Application.create(
|
||||
id: @application.id,
|
||||
channel: @channel.id,
|
||||
version: @target_version.to_s,
|
||||
installation_path: @installation_directory,
|
||||
wine_prefix_path: "",
|
||||
launch_command: "%COMMAND%",
|
||||
timestamp: Time.now.to_i
|
||||
)
|
||||
|
||||
message_requester(Worker::Request::STATUS_COMPLETE, data: { application: app })
|
||||
|
||||
puts "APPLICATION: #{@application.name} #{@application.id}:#{@channel.id}:#{@target_version} installed."
|
||||
end
|
||||
|
||||
def mark_application_uninstalled
|
||||
message_requester(Worker::Request::STATUS_COMPLETE, data: {})
|
||||
end
|
||||
|
||||
# helper functions
|
||||
|
||||
def message_requester(status = Worker::Request::STATUS_IN_PROGRESS, data:, error: nil)
|
||||
@worker.message_requester(request_id: @request_id, status: status, data: data, error: error)
|
||||
end
|
||||
|
||||
def update_status(label:, fraction:)
|
||||
message_requester(data: { status: { title: "TASK #{@application.name} (#{@channel.name})", label: label, fraction: fraction } } )
|
||||
end
|
||||
|
||||
def application_target_installation_directory
|
||||
target_application = @worker.settings.applications.find { |app| app.id == @application.id && app.channel == @channel.id }
|
||||
if target_application
|
||||
target_application.installation_path
|
||||
else
|
||||
format("%s/%s/%s", @worker.settings.preferences.application_installation_directory, @application.id, @channel.id)
|
||||
end
|
||||
end
|
||||
|
||||
def package_cache_path(package)
|
||||
directory = @worker.settings.preferences.launcher_package_cache_directory
|
||||
|
||||
if package.version?
|
||||
format("%s/%s/%s/%s", directory, @application.id, package.version.to_s, package.name)
|
||||
else
|
||||
format("%s/%s/%s", directory, @application.id, package.name)
|
||||
end
|
||||
end
|
||||
|
||||
def ignored_file?(path)
|
||||
IGNORED_FILES.any? { |regex| path.match?(regex) }
|
||||
end
|
||||
|
||||
def create_directory(path)
|
||||
path.gsub!("\\", "/")
|
||||
|
||||
# directory doesn't exists, create it!
|
||||
unless File.directory?(path)
|
||||
FileUtils.mkdir_p(path)
|
||||
|
||||
add_directory_to_file_index(path)
|
||||
end
|
||||
end
|
||||
|
||||
# add EXISTING file or directory to index
|
||||
def add_to_file_index(path)
|
||||
path.gsub!("\\", "/")
|
||||
|
||||
@file_index[path.downcase] = IndexedFile.new(File.directory?(path) ? :directory : :file, path)
|
||||
end
|
||||
|
||||
# add EXISTING directory to file index, walking up the directory tree until existing directory entries are found
|
||||
# NOTE: this will **NOT** "scan" the directory for new directories or files
|
||||
def add_directory_to_file_index(path)
|
||||
path.gsub!("\\", "/")
|
||||
|
||||
segments = path.split("/")
|
||||
sub_path = path
|
||||
until (file_index = @file_index[sub_path.downcase])
|
||||
@file_index[sub_path.downcase]
|
||||
|
||||
segments.pop
|
||||
|
||||
break if segments.empty?
|
||||
|
||||
sub_path = segments.join("/")
|
||||
end
|
||||
end
|
||||
|
||||
def remove_from_file_index(path)
|
||||
path.gsub!("\\", "/")
|
||||
|
||||
@file_index.delete(path.downcase)
|
||||
end
|
||||
|
||||
# And behold, the backslashes were slain and the path deemed sane!
|
||||
def normalize_path(base_path)
|
||||
base_path = base_path.gsub("\\", "/")
|
||||
path = "#{@installation_directory}/#{base_path}".gsub("\\", "/")
|
||||
|
||||
# try to find exact, existing match
|
||||
index_file = @file_index[path.downcase]
|
||||
return index_file.path if index_file
|
||||
|
||||
# try to find directory structure
|
||||
if (index_directory = @file_index[File.dirname(path).downcase])
|
||||
return format("%s/%s", index_directory.path, File.basename(path))
|
||||
end
|
||||
|
||||
# walk directories to see if we have a partial path match
|
||||
sub_paths = base_path.split("/")
|
||||
sub_paths.pop # drop file name from sub path reconstruction
|
||||
unless sub_paths.empty?
|
||||
partial_index_directory = nil
|
||||
sub_path = format("%s/%s", @installation_directory, sub_paths.shift).downcase
|
||||
while (partial_index_directory = @file_index[sub_path.downcase])
|
||||
sub_path = format("%s/%s", sub_path, sub_paths.shift)
|
||||
end
|
||||
|
||||
if partial_index_directory
|
||||
if sub_paths.empty? # we've found the path!
|
||||
return format("%s/%s", partial_index_directory.path, File.basename(base_path))
|
||||
else # we has a directory or two that don't exist, downcase them, and then head out to the pub!
|
||||
return format("%s/%s/%s", partial_index_directory.path, sub_paths.map(&:downcase).join("/"), File.basename(base_path))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# path doesn't exist, convert directories to lower-case, and use provided file case
|
||||
if File.basename(path) == base_path # base_path doesn't have a sub directory
|
||||
format("%s/%s", File.dirname(path), File.basename(path))
|
||||
else # base_path has sub directory/directories
|
||||
format("%s/%s/%s", @installation_directory, File.dirname(base_path).downcase, File.basename(path))
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_manifest(version)
|
||||
result = @worker.w3dhub_api.fetch_package_details([
|
||||
{
|
||||
category: "games",
|
||||
subcategory: @application.id,
|
||||
name: "manifest.xml",
|
||||
version: version
|
||||
}
|
||||
])
|
||||
|
||||
return result unless result.okay?
|
||||
|
||||
response = JSON.parse(result.data)
|
||||
manifest_package = response["packages"]&.map { |item| Worker::Api::LegacyManifestPackage.new(item) }&.first
|
||||
|
||||
if manifest_package.nil? || manifest_package.error?
|
||||
pp manifest_package
|
||||
abort_task!("Failed to fetch package details for manifest #{version}")
|
||||
end
|
||||
|
||||
# FIXME: check if locally downloaded manifest is still valid, if present.
|
||||
file_path = package_cache_path(manifest_package)
|
||||
|
||||
create_directory(File.dirname(file_path))
|
||||
|
||||
result = if manifest_package.download_url
|
||||
@worker.w3dhub_api.download(manifest_package.download_url, path: file_path)
|
||||
else
|
||||
# TODO xD
|
||||
@worker.w3dhub_api.fetch_package("TODO")
|
||||
end
|
||||
|
||||
if result.okay?
|
||||
return Worker::Api::LegacyManifest.new(package_cache_path(manifest_package))
|
||||
else
|
||||
pp result
|
||||
abort_task!("Failed to fetch manifest #{version}")
|
||||
end
|
||||
end
|
||||
|
||||
def apply_patch(manifest_file, package)
|
||||
Tempfile.create do |f|
|
||||
package_path = package_cache_path(package)
|
||||
|
||||
puts "unpacking patch..."
|
||||
unzip(package_path, f.path)
|
||||
|
||||
puts "reading patch data.."
|
||||
patch_mix = W3DHubLauncher::WWMix.new(path: f.path)
|
||||
raise patch_mix.error_reason unless patch_mix.load
|
||||
|
||||
patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") }
|
||||
patch_entry.read
|
||||
# "remove" patch meta file from patch before copying patch data
|
||||
patch_mix.entries.delete(patch_entry)
|
||||
|
||||
patch_info = JSON.parse(patch_entry.blob)
|
||||
|
||||
puts "loading target mix metadata... (#{normalize_path(manifest_file.name)})"
|
||||
target_mix = W3DHubLauncher::WWMix.new(path: normalize_path(manifest_file.name))
|
||||
raise target_mix.error_reason unless target_mix.load
|
||||
|
||||
patch_info["removedFiles"].each do |file|
|
||||
puts "removing file from target: #{file}"
|
||||
target_mix.entries.delete_if { |e| e.name.casecmp?(file) }
|
||||
end
|
||||
|
||||
patch_info["updatedFiles"].each do |file|
|
||||
puts "adding/updating file from target: #{file}"
|
||||
patch_mix.entries.each do |entry|
|
||||
target_mix.add_entry(entry: entry, replace: true)
|
||||
end
|
||||
end
|
||||
|
||||
Tempfile.create do |temp|
|
||||
temp_mix = W3DHubLauncher::WWMix.new(path: temp.path, encrypted: target_mix.encrypted?)
|
||||
target_mix.entries.each { |e| temp_mix.add_entry(entry: e, replace: true) }
|
||||
raise temp_mix.error_reason unless temp_mix.save
|
||||
|
||||
puts "copying file..."
|
||||
FileUtils.cp(temp.path, normalize_path(manifest_file.name))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def unzip(zip_path, output_path = nil)
|
||||
stream = Zip::InputStream.new(File.open(zip_path))
|
||||
|
||||
while(entry = stream.get_next_entry)
|
||||
file_path = output_path || normalize_path(entry.name)
|
||||
|
||||
next if ignored_file?(file_path)
|
||||
|
||||
pp file_path
|
||||
|
||||
create_directory(File.dirname(file_path)) unless output_path
|
||||
|
||||
File.open(file_path, "wb") do |f|
|
||||
entry_stream = entry.get_input_stream
|
||||
|
||||
while(chunk = entry_stream.read(4_194_304))
|
||||
f.write(chunk)
|
||||
end
|
||||
end
|
||||
|
||||
add_to_file_index(file_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# def execute_task
|
||||
# show_application_taskbar
|
||||
|
||||
# fail_fast
|
||||
# return false if failed?
|
||||
|
||||
# fetch_manifests
|
||||
# return false if failed?
|
||||
|
||||
# build_package_list
|
||||
# return false if failed?
|
||||
|
||||
# remove_deleted_files
|
||||
# return false if failed?
|
||||
|
||||
# verify_files
|
||||
# return false if failed?
|
||||
|
||||
# fetch_packages
|
||||
# return false if failed?
|
||||
|
||||
# verify_packages
|
||||
# return false if failed?
|
||||
|
||||
# unpack_packages
|
||||
# return false if failed?
|
||||
|
||||
# create_wine_prefix
|
||||
# return false if failed?
|
||||
|
||||
# install_dependencies
|
||||
# return false if failed?
|
||||
|
||||
# write_paths_ini
|
||||
# return false if failed?
|
||||
|
||||
# mark_application_installed
|
||||
# return false if failed?
|
||||
|
||||
# sleep 1
|
||||
# hide_application_taskbar
|
||||
|
||||
# true
|
||||
# end
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
module W3DHubLauncher
|
||||
class Task
|
||||
class InstallApplication < Task
|
||||
def setup
|
||||
end
|
||||
|
||||
def execute
|
||||
# TODO:
|
||||
# fail_fast
|
||||
|
||||
# get case-insensitive file list
|
||||
index_installation_directory
|
||||
|
||||
# fetch manifests for currently installed version
|
||||
# AND the target version so we can detect files that need to be removed
|
||||
# between versions
|
||||
fetch_manifests
|
||||
|
||||
# build list of packages we'll need to download
|
||||
build_package_list
|
||||
|
||||
# verify our pre-existing files for presence and validity
|
||||
verify_files
|
||||
|
||||
# download required packages to get missing or out of date files
|
||||
fetch_packages
|
||||
|
||||
# unpack packages and patch files to reach target version
|
||||
install_packages
|
||||
|
||||
# delete files that are no longer part of the application (and whose presence may break the application)
|
||||
remove_deleted_files
|
||||
|
||||
# tell the application to behive
|
||||
write_paths_ini
|
||||
|
||||
# store application install state and notify frontend/ui that the application is ready for use!
|
||||
mark_application_installed
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
0
lib/worker/tasks/move_application.rb
Normal file
0
lib/worker/tasks/move_application.rb
Normal file
@@ -6,13 +6,17 @@ module W3DHubLauncher
|
||||
PRIMARY_W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze
|
||||
ALTERNATIVE_W3DHUB_API_ENDPOINT = "https://backend.w3d.cyberarm.dev".freeze
|
||||
|
||||
attr_reader :applications
|
||||
|
||||
def initialize
|
||||
@access_token = nil
|
||||
|
||||
@applications = []
|
||||
|
||||
@http_clients = {}
|
||||
end
|
||||
|
||||
def headers(form_encoded: false)
|
||||
def headers(form_encoded: false, range: nil)
|
||||
array = [
|
||||
["user-agent", W3DHubLauncher::USER_AGENT],
|
||||
["accept", "application/json"],
|
||||
@@ -20,6 +24,7 @@ module W3DHubLauncher
|
||||
|
||||
array << ["content-type", "application/x-www-form-urlencoded"] if form_encoded
|
||||
array << ["authorization", "Bearer #{@access_token}"] if @access_token
|
||||
array << ["range", "bytes=#{range}-"] if range
|
||||
|
||||
# pp array
|
||||
|
||||
@@ -33,10 +38,10 @@ module W3DHubLauncher
|
||||
Sync do |task|
|
||||
task.with_timeout(API_TIMEOUT) do
|
||||
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
||||
pp [method, url, headers, body, response]
|
||||
if response.success?
|
||||
result.data = response.read
|
||||
else
|
||||
# pp response
|
||||
result.error = true
|
||||
end
|
||||
end
|
||||
@@ -57,27 +62,28 @@ module W3DHubLauncher
|
||||
result = CyberarmEngine::Result.new
|
||||
|
||||
Sync do |task|
|
||||
task.with_timeout(API_TIMEOUT) do
|
||||
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
||||
if response.success?
|
||||
content_length = response.headers["content-length"] || 0
|
||||
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
|
||||
total_downloaded_bytes = 0
|
||||
range = Integer(headers.find { |key, value| key == "range" }&.last&.split("=")&.last&.split("-")&.first || 0)
|
||||
File.open(path, range.positive? ? "r+b" : "wb") do |file|
|
||||
file.pos = range if range.positive?
|
||||
|
||||
block&.call(downloaded_bytes, total_downloaded_bytes, content_length)
|
||||
end
|
||||
response.each do |chunk|
|
||||
file.write(chunk)
|
||||
downloaded_bytes = chunk.length
|
||||
total_downloaded_bytes += downloaded_bytes
|
||||
|
||||
block&.call(downloaded_bytes, total_downloaded_bytes, content_length)
|
||||
end
|
||||
|
||||
result.data = true
|
||||
end
|
||||
rescue StandardError => e
|
||||
result.error = e
|
||||
|
||||
result.data = true
|
||||
end
|
||||
rescue StandardError => e
|
||||
result.error = e
|
||||
rescue Async::TimeoutError
|
||||
result.error = e
|
||||
end
|
||||
@@ -104,47 +110,56 @@ module W3DHubLauncher
|
||||
primary_result = @access_token ? fetch("#{PRIMARY_W3DHUB_API_ENDPOINT}/apis/launcher/1/get-applications") : CyberarmEngine::Result.new(error: true)
|
||||
alternate_result = fetch("#{ALTERNATIVE_W3DHUB_API_ENDPOINT}/apis/launcher/1/get-applications")
|
||||
|
||||
single_source = false
|
||||
|
||||
# We've failed to retrieve data
|
||||
if primary_result.error? && alternate_result.error?
|
||||
return result
|
||||
# We've only gotten data back from the primary backend
|
||||
elsif primary_result.okay? && alternate_result.error?
|
||||
result.data = primary_result.data
|
||||
return result
|
||||
single_source = true
|
||||
# We've only gotten data back from the alternate backend
|
||||
elsif primary_result.error? && alternate_result.okay?
|
||||
result.data = alternate_result.data
|
||||
return result
|
||||
single_source = true
|
||||
end
|
||||
|
||||
# We've gotten data back from both backends, merge them.
|
||||
# pp [primary_result, alternate_result]
|
||||
unless single_source
|
||||
puts "FIXME: merge applications source"
|
||||
# We've gotten data back from both backends, merge them.
|
||||
# pp [primary_result, alternate_result]
|
||||
|
||||
# FIXME: Merge primary and alternate results
|
||||
end
|
||||
|
||||
@applications = JSON.parse(result.data)["applications"]&.map { |app| W3DHubLauncher::Worker::Api::Application.new(app) } || []
|
||||
|
||||
# FIXME: Merge primary and alternate results
|
||||
result
|
||||
end
|
||||
|
||||
def fetch_news()
|
||||
result = CyberarmEngine::Result.new
|
||||
def fetch_news(category)
|
||||
fetch("#{PRIMARY_W3DHUB_API_ENDPOINT}/apis/w3dhub/1/get-news", method: :post, body: "data={\"category\":\"#{category}\"}", headers: headers(form_encoded: true))
|
||||
end
|
||||
|
||||
def fetch_events()
|
||||
result = CyberarmEngine::Result.new
|
||||
def fetch_events(category)
|
||||
fetch("#{PRIMARY_W3DHUB_API_ENDPOINT}/apis/w3dhub/1/get-server-events", method: :post, body: "data={\"serverPath\":\"#{category}\"}", headers: headers(form_encoded: true))
|
||||
end
|
||||
|
||||
def fetch_test_events(category)
|
||||
fetch("#{PRIMARY_W3DHUB_API_ENDPOINT}/apis/w3dhub/1/get-testing-times")
|
||||
end
|
||||
|
||||
def fetch_manifest()
|
||||
result = CyberarmEngine::Result.new
|
||||
end
|
||||
|
||||
def fetch_manifests()
|
||||
result = CyberarmEngine::Result.new
|
||||
def fetch_package_details(packages)
|
||||
body = URI.encode_www_form("data": JSON.dump({ packages: packages }))
|
||||
fetch("#{@access_token ? PRIMARY_W3DHUB_API_ENDPOINT : ALTERNATIVE_W3DHUB_API_ENDPOINT}/apis/launcher/1/get-package-details", method: :post, body: body, headers: headers(form_encoded: true))
|
||||
end
|
||||
|
||||
def fetch_package_details()
|
||||
result = CyberarmEngine::Result.new
|
||||
end
|
||||
|
||||
def fetch_package()
|
||||
def download_package()
|
||||
download()
|
||||
end
|
||||
end
|
||||
|
||||
BIN
media/default_game_cover.png
Normal file
BIN
media/default_game_cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
media/default_map_preview.png
Normal file
BIN
media/default_map_preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -11,6 +11,7 @@ require "logger"
|
||||
require "digest"
|
||||
require "pathname"
|
||||
require "fileutils"
|
||||
require "tempfile"
|
||||
|
||||
require "async"
|
||||
require "async/http/internet/instance"
|
||||
@@ -39,11 +40,14 @@ require_relative "lib/dialogs/about"
|
||||
require_relative "lib/dialogs/downloads"
|
||||
require_relative "lib/dialogs/server"
|
||||
require_relative "lib/dialogs/account"
|
||||
require_relative "lib/dialogs/import_application"
|
||||
require_relative "lib/states/boot"
|
||||
require_relative "lib/states/interface"
|
||||
require_relative "lib/window"
|
||||
require_relative "lib/application_helper"
|
||||
|
||||
require_relative "lib/ico"
|
||||
require_relative "lib/dds"
|
||||
require_relative "lib/ww_mix"
|
||||
|
||||
require_relative "lib/worker"
|
||||
@@ -51,6 +55,11 @@ require_relative "lib/worker/api"
|
||||
require_relative "lib/worker/api/application"
|
||||
require_relative "lib/worker/api/game_server"
|
||||
require_relative "lib/worker/api/settings"
|
||||
require_relative "lib/worker/api/news_item"
|
||||
require_relative "lib/worker/api/server_event"
|
||||
require_relative "lib/worker/api/test_event"
|
||||
require_relative "lib/worker/api/legacy_manifest"
|
||||
require_relative "lib/worker/api/legacy_manifest_package"
|
||||
require_relative "lib/worker/request"
|
||||
require_relative "lib/worker/w3dhub_api"
|
||||
require_relative "lib/worker/task"
|
||||
@@ -58,6 +67,7 @@ require_relative "lib/worker/tasks/install_application"
|
||||
require_relative "lib/worker/tasks/uninstall_application"
|
||||
require_relative "lib/worker/tasks/repair_application"
|
||||
require_relative "lib/worker/tasks/update_application"
|
||||
require_relative "lib/worker/tasks/move_application"
|
||||
|
||||
module W3DHubLauncher
|
||||
MemCache = {}
|
||||
|
||||
Reference in New Issue
Block a user