mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-09-22 08:13:48 +00:00
Compare commits
58 Commits
9d0a99de6e
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
| 2737336c5d | |||
| 9348367994 | |||
| 80954522e5 | |||
| 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 | |||
| 915e643a98 | |||
| 6823f9bade | |||
| 8c14ecffc1 | |||
| a7ae460446 | |||
| 969a82a262 | |||
| efc9c2f150 | |||
| 9ad43f61b6 | |||
| ccea626852 | |||
| b589a3dc1b | |||
| 8333c7b00d | |||
| 7f716a57ca | |||
| 7524d01347 | |||
| a4dcd302c1 | |||
| 8dd9b44cb0 | |||
| 60672ef9c3 | |||
| aeaf0b377b | |||
| ad0b9dbe7b | |||
| 1f33e125f6 | |||
| ba055897bc | |||
| baec1e1e18 | |||
| 59d884aad1 | |||
| d77408484a | |||
| c6ee9eab31 | |||
| 02cbd215f5 | |||
| 675ab3b1a8 | |||
| 0eb188f72e | |||
| c8bb271d82 | |||
| f1d6bbe390 | |||
| 56bb6a59e2 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
|||||||
pkg/
|
pkg/
|
||||||
*.json
|
*.json
|
||||||
|
*.xml
|
||||||
data/cache/*
|
data/cache/*
|
||||||
!data/cache/.gitkeep
|
!data/cache/.gitkeep
|
||||||
data/logs/*
|
data/logs/*
|
||||||
|
|||||||
5
Gemfile
5
Gemfile
@@ -4,10 +4,12 @@ source "https://gem.coop"
|
|||||||
gem "base64"
|
gem "base64"
|
||||||
gem "rexml"
|
gem "rexml"
|
||||||
gem "logger"
|
gem "logger"
|
||||||
|
gem "json"
|
||||||
|
|
||||||
# "game" library gem
|
# "game" library gem
|
||||||
gem "cyberarm_engine"
|
gem "cyberarm_engine"
|
||||||
gem "sdl2-bindings"
|
gem "sdl3-bindings"
|
||||||
|
gem "gosu", '1.5.0' # temporary, manually built, stand-in for SDL3 supporting gosu
|
||||||
|
|
||||||
# networking libs
|
# networking libs
|
||||||
gem "async"
|
gem "async"
|
||||||
@@ -18,3 +20,4 @@ gem "async-websocket"
|
|||||||
gem "digest-crc"
|
gem "digest-crc"
|
||||||
gem "ircparser"
|
gem "ircparser"
|
||||||
gem "rubyzip"
|
gem "rubyzip"
|
||||||
|
gem "chunky_png"
|
||||||
|
|||||||
117
Gemfile.lock
117
Gemfile.lock
@@ -1,32 +1,29 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://gem.coop/
|
remote: https://gem.coop/
|
||||||
specs:
|
specs:
|
||||||
async (2.39.0)
|
async (2.46.0)
|
||||||
console (~> 1.29)
|
console (~> 1.29)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
io-event (~> 1.11)
|
io-event (~> 1.21)
|
||||||
metrics (~> 0.12)
|
async-http (0.105.0)
|
||||||
traces (~> 0.18)
|
async (>= 2.35.1)
|
||||||
async-http (0.95.0)
|
async-pool (~> 0.12)
|
||||||
async (>= 2.10.2)
|
io-endpoint (~> 0.18)
|
||||||
async-pool (~> 0.11)
|
io-stream (~> 0.14)
|
||||||
io-endpoint (~> 0.14)
|
protocol-http (~> 0.72)
|
||||||
io-stream (~> 0.6)
|
protocol-http1 (~> 0.41)
|
||||||
metrics (~> 0.12)
|
protocol-http2 (~> 0.28)
|
||||||
protocol-http (~> 0.62)
|
|
||||||
protocol-http1 (~> 0.39)
|
|
||||||
protocol-http2 (~> 0.26)
|
|
||||||
protocol-url (~> 0.2)
|
protocol-url (~> 0.2)
|
||||||
traces (~> 0.10)
|
async-pool (0.12.0)
|
||||||
async-pool (0.11.2)
|
|
||||||
async (>= 2.0)
|
async (>= 2.0)
|
||||||
async-websocket (0.30.0)
|
async-websocket (0.30.1)
|
||||||
async-http (~> 0.76)
|
async-http (~> 0.76)
|
||||||
protocol-http (~> 0.34)
|
protocol-http (~> 0.34)
|
||||||
protocol-rack (~> 0.7)
|
protocol-rack (~> 0.7)
|
||||||
protocol-websocket (~> 0.17)
|
protocol-websocket (~> 0.17)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
console (1.34.3)
|
chunky_png (1.4.0)
|
||||||
|
console (1.38.0)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
fiber-local (~> 1.1)
|
fiber-local (~> 1.1)
|
||||||
json
|
json
|
||||||
@@ -39,35 +36,38 @@ GEM
|
|||||||
fiber-local (1.1.0)
|
fiber-local (1.1.0)
|
||||||
fiber-storage
|
fiber-storage
|
||||||
fiber-storage (1.0.1)
|
fiber-storage (1.0.1)
|
||||||
gosu (1.4.6)
|
gosu (1.5.0-x86_64-linux)
|
||||||
io-endpoint (0.17.2)
|
ffi (~> 1.12)
|
||||||
io-event (1.15.1)
|
io-endpoint (0.18.0)
|
||||||
io-stream (0.11.1)
|
openssl (>= 3.3.0)
|
||||||
|
io-event (1.22.0)
|
||||||
|
io-stream (0.14.0)
|
||||||
|
openssl (>= 3.3)
|
||||||
ircparser (1.0.0)
|
ircparser (1.0.0)
|
||||||
json (2.19.4)
|
json (3.0.2)
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
metrics (0.15.0)
|
openssl (4.0.2)
|
||||||
protocol-hpack (1.5.1)
|
protocol-hpack (1.5.1)
|
||||||
protocol-http (0.62.2)
|
protocol-http (0.72.0)
|
||||||
protocol-http1 (0.39.0)
|
protocol-http1 (0.41.0)
|
||||||
protocol-http (~> 0.62)
|
protocol-http (~> 0.68)
|
||||||
protocol-http2 (0.26.0)
|
protocol-http2 (0.28.0)
|
||||||
protocol-hpack (~> 1.4)
|
protocol-hpack (~> 1.4)
|
||||||
protocol-http (~> 0.62)
|
protocol-http (~> 0.62)
|
||||||
protocol-rack (0.22.1)
|
protocol-rack (0.23.0)
|
||||||
|
console (~> 1.0)
|
||||||
io-stream (>= 0.10)
|
io-stream (>= 0.10)
|
||||||
protocol-http (~> 0.58)
|
protocol-http (~> 0.58)
|
||||||
rack (>= 1.0)
|
rack (>= 1.0)
|
||||||
protocol-url (0.4.0)
|
protocol-url (0.19.0)
|
||||||
protocol-websocket (0.21.0)
|
protocol-websocket (0.21.2)
|
||||||
protocol-http (~> 0.2)
|
protocol-http (~> 0.2)
|
||||||
rack (3.2.6)
|
rack (3.2.7)
|
||||||
rake (13.4.2)
|
rake (13.4.2)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.6.0)
|
||||||
sdl2-bindings (0.2.3)
|
sdl3-bindings (1.0.4)
|
||||||
ffi (~> 1.15)
|
ffi (~> 1.16)
|
||||||
traces (0.18.2)
|
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
@@ -77,48 +77,51 @@ DEPENDENCIES
|
|||||||
async-http
|
async-http
|
||||||
async-websocket
|
async-websocket
|
||||||
base64
|
base64
|
||||||
|
chunky_png
|
||||||
cyberarm_engine
|
cyberarm_engine
|
||||||
digest-crc
|
digest-crc
|
||||||
|
gosu (= 1.5.0)
|
||||||
ircparser
|
ircparser
|
||||||
|
json
|
||||||
logger
|
logger
|
||||||
rexml
|
rexml
|
||||||
rubyzip
|
rubyzip
|
||||||
sdl2-bindings
|
sdl3-bindings
|
||||||
|
|
||||||
CHECKSUMS
|
CHECKSUMS
|
||||||
async (2.39.0) sha256=df18730073f2bbb45788077dfa20cb365ecc1b9453969f44de6796b5191a00aa
|
async (2.46.0) sha256=34f984733dbf63a997376346451ec995e858cacc3105e86ccfeaf8877b4f490b
|
||||||
async-http (0.95.0) sha256=08128cab255a48e41d1e856bfa9cbda001f1c57c88bafa3fa3a545ad795754aa
|
async-http (0.105.0) sha256=55191969d5cf61be5fbc2e0aadd51d77085318ba192a5ff5e12e3aba0826906a
|
||||||
async-pool (0.11.2) sha256=0a43a17b02b04d9c451b7d12fafa9a50e55dc6dd00d4369aca00433f16a7e3ed
|
async-pool (0.12.0) sha256=5580493dc024bd704dab2ed0bf1307100722e49cbb825c7965f8501194f58506
|
||||||
async-websocket (0.30.0) sha256=55739954528ad8f87f7792d0452e1268d1ef2aa5b3719f79400a05a1a6202cdf
|
async-websocket (0.30.1) sha256=54bb8a8f184e4aa64434c7a78ecc55850a67a3d1dcd02e3ae787376e2c673936
|
||||||
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
||||||
console (1.34.3) sha256=869fbd74697efc4c606f102d2812b0b008e4e7fd738a91c591e8577140ec0dcc
|
chunky_png (1.4.0) sha256=89d5b31b55c0cf4da3cf89a2b4ebc3178d8abe8cbaf116a1dba95668502fdcfe
|
||||||
|
console (1.38.0) sha256=35b82f59130e95e9be6b832d9e539c8b3988fc192e4a8e9a86ab4922f44ad5de
|
||||||
cyberarm_engine (0.25.1) sha256=6f851a2ed0a32bdd25fc619172b546a8e26eefeb13a62c509f226337170d0efe
|
cyberarm_engine (0.25.1) sha256=6f851a2ed0a32bdd25fc619172b546a8e26eefeb13a62c509f226337170d0efe
|
||||||
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
|
digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07
|
||||||
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
ffi (1.17.4-x86_64-linux-gnu) sha256=9d3db14c2eae074b382fa9c083fe95aec6e0a1451da249eab096c34002bc752d
|
||||||
fiber-annotation (0.2.0) sha256=7abfadf1d119f508867d4103bf231c0354d019cc39a5738945dec2edadaf6c03
|
fiber-annotation (0.2.0) sha256=7abfadf1d119f508867d4103bf231c0354d019cc39a5738945dec2edadaf6c03
|
||||||
fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06
|
fiber-local (1.1.0) sha256=c885f94f210fb9b05737de65d511136ea602e00c5105953748aa0f8793489f06
|
||||||
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
|
fiber-storage (1.0.1) sha256=f48e5b6d8b0be96dac486332b55cee82240057065dc761c1ea692b2e719240e1
|
||||||
gosu (1.4.6) sha256=80c7fa48cf61f4960ef067fdd1c000d98b4adc8e11faeaa486543d00908a5c15
|
gosu (1.5.0-x86_64-linux)
|
||||||
io-endpoint (0.17.2) sha256=3feaf766c116b35839c11fac68b6aaadc47887bb488902a57bf8e1d288fb3338
|
io-endpoint (0.18.0) sha256=720d186bcd584e8ba490bfc91dab332cc3c5ffee848b5b03894555cb275151aa
|
||||||
io-event (1.15.1) sha256=c644cdcf48254015d63f558bf4492f35471f5bb204a42180ea49752be59b30cc
|
io-event (1.22.0) sha256=4936d38dfcf2291d897707fd94690f1a63fdb1ea3ab4fa63c6739fc6c61e1534
|
||||||
io-stream (0.11.1) sha256=fa5f551fcff99581c1757b9d1cee2c37b124f07d2ca4f40b756a05ab9bd21b87
|
io-stream (0.14.0) sha256=07d9b13ea2066ccbbc761fff8d69ad5a31e47561d31e02cc271f3ed9c1a8f1cd
|
||||||
ircparser (1.0.0) sha256=0d4598f691172e2ad078207c50ba62c0d3428ae841836b0a682e6cd01d62878d
|
ircparser (1.0.0) sha256=0d4598f691172e2ad078207c50ba62c0d3428ae841836b0a682e6cd01d62878d
|
||||||
json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
|
json (3.0.2) sha256=8e6d7e7b11384c21230430cef90b71f14849a34a1f4452796670f7c981bd19df
|
||||||
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
||||||
metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
|
openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2
|
||||||
protocol-hpack (1.5.1) sha256=6feca238b8078da1cd295677d6f306c6001af92d75fe0643d33e6956cbc3ad91
|
protocol-hpack (1.5.1) sha256=6feca238b8078da1cd295677d6f306c6001af92d75fe0643d33e6956cbc3ad91
|
||||||
protocol-http (0.62.2) sha256=e1c1f2f56029c1af8c4e2b8a67d0d096c76620f3afd8d99d1dcd2f6b8ffa773b
|
protocol-http (0.72.0) sha256=b153474785979495b9f2df78f0fdc2e02edc7e92c1f7ea00425eed73bd02194c
|
||||||
protocol-http1 (0.39.0) sha256=e49b3f4cda6f5d94c76a323d2b7f6977cba3ebd082d2da437039594da77ad8eb
|
protocol-http1 (0.41.0) sha256=9863874a1228948a896c3e7ea16d13a680c8f3ed50706007c78b7033018b0ea3
|
||||||
protocol-http2 (0.26.0) sha256=bac89cd78082b241ccd0cf7246f5160e4bb0c9c975fb4bf7deef5f88cc317486
|
protocol-http2 (0.28.0) sha256=b7c6195dcfa80ec2484670e9fd8fc21024cc654a391aa0792433aff1188c1691
|
||||||
protocol-rack (0.22.1) sha256=1185d245927ef9849a603700d6991ca353bc89724fbf98efa4a4333ed62a9fc3
|
protocol-rack (0.23.0) sha256=63eec3341b20565b3dd155345fd1426ecbbed322cdc8a6accc993905388c03d7
|
||||||
protocol-url (0.4.0) sha256=64d4c03b6b51ad815ac6fdaf77a1d91e5baf9220d26becb846c5459dacdea9e1
|
protocol-url (0.19.0) sha256=3f6b9f6aa742a259a0b3610fbbd9685dcd9506adf6734f428ff96259faabfb21
|
||||||
protocol-websocket (0.21.0) sha256=6e2ccc2adf7de1895b0f6548fdfacf5f9735c0d4deb56cd2bac1ae38a1952e93
|
protocol-websocket (0.21.2) sha256=cbe35be8e045597b221874fba5928d7cd2483884126e3e36faece4741dc88eb7
|
||||||
rack (3.2.6) sha256=5ed78e1f73b2e25679bec7d45ee2d4483cc4146eb1be0264fc4d94cb5ef212c2
|
rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7
|
||||||
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
||||||
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
|
||||||
rubyzip (3.2.2) sha256=c0ed99385f0625415c8f05bcae33fe649ed2952894a95ff8b08f26ca57ea5b3c
|
rubyzip (3.6.0) sha256=268994d44d62282d1cfd99bf10eae48d7267199158ad7ea3e1fee2da9458b695
|
||||||
sdl2-bindings (0.2.3) sha256=878c0755a4bea1aed0fa85c01596effd1c2b7d1878141ceec5bef808038bd309
|
sdl3-bindings (1.0.4) sha256=326f497c30e6a5cab402e6178403b30c9df4a11a4e109e08c5da6e920c7bb55b
|
||||||
traces (0.18.2) sha256=80f1649cb4daace1d7174b81f3b3b7427af0b93047759ba349960cb8f315e214
|
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.8
|
4.0.8
|
||||||
|
|||||||
75
lib/application_helper.rb
Normal file
75
lib/application_helper.rb
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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 = case app.id
|
||||||
|
when "ecw" # ecw is a little special. each of its numbered exes represents a max render distance.
|
||||||
|
format("%s/game500.exe", app.installation_path)
|
||||||
|
when "ren" # little hacky. TT auto patching launcher doesn't work* on linux so bypass it.
|
||||||
|
format("%s/game2.exe", app.installation_path)
|
||||||
|
else # the rest of the games are reasonably sane.
|
||||||
|
format("%s/game.exe", app.installation_path)
|
||||||
|
end
|
||||||
|
|
||||||
|
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 = [
|
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("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("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("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"),
|
Item.new("async", "Asynchronous event-driven reactor for Ruby", "https://github.com/socketry/async", "MIT", "https://github.com/socketry/async/blob/main/license.md"),
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
module W3DHubLauncher
|
module W3DHubLauncher
|
||||||
ROOT_PATH = Dir.pwd
|
DIRECTORY_NAME = "w3d-hub-linux-launcher".freeze
|
||||||
|
|
||||||
|
ROOT_PATH = Dir.pwd
|
||||||
|
CONFIG_PATH = "#{Dir.home}/.config/#{DIRECTORY_NAME}".freeze
|
||||||
|
CACHE_PATH = "#{Dir.home}/.cache/#{DIRECTORY_NAME}".freeze
|
||||||
|
DEFAULT_PACKAGE_CACHE_PATH = "#{CACHE_PATH}/packages".freeze
|
||||||
|
DEFAULT_APPLICATIONS_PATH = "#{Dir.home}/.local/share/#{DIRECTORY_NAME}/applications".freeze
|
||||||
|
|
||||||
|
IPC_PATH = "/tmp/w3dhub_linux_launcher.sock"
|
||||||
USER_AGENT = "#{NAME} v#{VERSION}".freeze
|
USER_AGENT = "#{NAME} v#{VERSION}".freeze
|
||||||
end
|
end
|
||||||
|
|||||||
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
|
||||||
@@ -3,7 +3,7 @@ module W3DHubLauncher
|
|||||||
def setup
|
def setup
|
||||||
theme(THEME)
|
theme(THEME)
|
||||||
|
|
||||||
background 0xee_222222
|
background TRANSLUCENT_GRAY
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,16 +4,16 @@ module W3DHubLauncher
|
|||||||
def setup
|
def setup
|
||||||
super
|
super
|
||||||
|
|
||||||
stack(width: 1.0, max_width: 600, height: 1.0, max_height: 600, h_align: :center, v_align: :center, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
|
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: ALPHA_GRAY) 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 "About", width: 1.0, text_align: :center
|
banner "About", width: 1.0, text_align: :center
|
||||||
end
|
end
|
||||||
|
|
||||||
stack(width: 1.0, fill: true, scroll: true, padding_left: PADDING, padding_right: PADDING) do
|
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 NAME
|
title NAME
|
||||||
tagline format("v%s (%s)", VERSION, VERSION_NAME), margin_left: PADDING
|
tagline format("v%s (%s)", VERSION, VERSION_NAME), margin_left: PADDING
|
||||||
para "© 2021 - #{Time.now.year} cyberarm", 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
|
title "Special Thanks", margin_top: LARGE_PADDING
|
||||||
W3DHubLauncher::Attribution::SPECIAL_THANKS.each do |item|
|
W3DHubLauncher::Attribution::SPECIAL_THANKS.each do |item|
|
||||||
@@ -26,10 +26,10 @@ module W3DHubLauncher
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
flow(width: 1.0, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
flow(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
|
||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
|
|
||||||
button "Close" do
|
button "Close", **CTA_BUTTON_THEME do
|
||||||
pop_state
|
pop_state
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -40,12 +40,18 @@ module W3DHubLauncher
|
|||||||
|
|
||||||
def present_item(item)
|
def present_item(item)
|
||||||
unless item.url.empty?
|
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
|
else
|
||||||
tagline item.name, margin_left: PADDING
|
tagline item.name, margin_left: PADDING
|
||||||
end
|
end
|
||||||
para item.description, margin_left: LARGE_PADDING
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
25
lib/dialogs/account.rb
Normal file
25
lib/dialogs/account.rb
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module W3DHubLauncher
|
||||||
|
class Dialog
|
||||||
|
class Account < W3DHubLauncher::Dialog
|
||||||
|
def setup
|
||||||
|
super
|
||||||
|
|
||||||
|
stack(width: 1.0, max_width: 600, height: 1.0, max_height: 600, h_align: :center, v_align: :center, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
|
||||||
|
flow(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||||
|
banner "Account", width: 1.0, text_align: :center
|
||||||
|
end
|
||||||
|
|
||||||
|
stack(width: 1.0, fill: true, scroll: true, padding_left: PADDING, padding_right: PADDING) do
|
||||||
|
button "Sign In", tip: "https://sso.w3d.cyberarm.dev" do
|
||||||
|
# use SSO service
|
||||||
|
pop_state
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flow(width: 1.0, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
38
lib/dialogs/downloads.rb
Normal file
38
lib/dialogs/downloads.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module W3DHubLauncher
|
||||||
|
class Dialog
|
||||||
|
class Downloads < W3DHubLauncher::Dialog
|
||||||
|
def setup
|
||||||
|
super
|
||||||
|
|
||||||
|
stack(width: 1.0, max_width: 600, height: 1.0, max_height: 600, h_align: :center, v_align: :center, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
|
||||||
|
flow(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||||
|
banner "Downloads", width: 1.0, text_align: :center
|
||||||
|
end
|
||||||
|
|
||||||
|
stack(width: 1.0, fill: true, scroll: true, padding_left: PADDING, padding_right: PADDING) do
|
||||||
|
50.times do
|
||||||
|
stack(width: 1.0, margin_top: HALF_PADDING, margin_bottom: HALF_PADDING) do
|
||||||
|
flow(width: 1.0) do
|
||||||
|
para "PACKAGE_NAME.mix"
|
||||||
|
flow(fill: true)
|
||||||
|
para ["Downloading...", "Pending...", "Done.", "Patching...", "Unpacking..."].sample
|
||||||
|
end
|
||||||
|
progress(width: 1.0, fraction: rand)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flow(width: 1.0, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||||
|
flow(fill: true)
|
||||||
|
|
||||||
|
button "Close" do
|
||||||
|
pop_state
|
||||||
|
end
|
||||||
|
|
||||||
|
flow(fill: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
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
|
||||||
38
lib/dialogs/server.rb
Normal file
38
lib/dialogs/server.rb
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
module W3DHubLauncher
|
||||||
|
class Dialog
|
||||||
|
class Server < W3DHubLauncher::Dialog
|
||||||
|
def setup
|
||||||
|
super
|
||||||
|
|
||||||
|
stack(width: 1.0, max_width: 600, height: 1.0, max_height: 600, h_align: :center, v_align: :center, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_BLACK) do
|
||||||
|
flow(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||||
|
banner "Downloads", width: 1.0, text_align: :center
|
||||||
|
end
|
||||||
|
|
||||||
|
stack(width: 1.0, fill: true, scroll: true, padding_left: PADDING, padding_right: PADDING) do
|
||||||
|
50.times do
|
||||||
|
stack(width: 1.0, margin_top: HALF_PADDING, margin_bottom: HALF_PADDING) do
|
||||||
|
flow(width: 1.0) do
|
||||||
|
para "PACKAGE_NAME.mix"
|
||||||
|
flow(fill: true)
|
||||||
|
para ["Downloading...", "Pending...", "Done.", "Patching...", "Unpacking..."].sample
|
||||||
|
end
|
||||||
|
progress(width: 1.0, fraction: rand)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flow(width: 1.0, padding: PADDING, background_nine_slice: NINE_SLICE_ROUNDED_BOTTOM, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) do
|
||||||
|
flow(fill: true)
|
||||||
|
|
||||||
|
button "Close" do
|
||||||
|
pop_state
|
||||||
|
end
|
||||||
|
|
||||||
|
flow(fill: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
36
lib/gosu_ext.rb
Normal file
36
lib/gosu_ext.rb
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
module Gosu
|
||||||
|
# ! nonblocking
|
||||||
|
def self.choose_file(&block)
|
||||||
|
callback_id = "gc_protect_#{Gosu.milliseconds}"
|
||||||
|
|
||||||
|
callback = lambda do |user_data, file_list, filter|
|
||||||
|
data = nil
|
||||||
|
|
||||||
|
if file_list
|
||||||
|
data = file_list.read_array_of_string
|
||||||
|
end
|
||||||
|
|
||||||
|
block.call(data)
|
||||||
|
|
||||||
|
W3DHubLauncher::MemCache.delete(callback_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
W3DHubLauncher::MemCache[callback_id] = callback
|
||||||
|
|
||||||
|
SDL.ShowOpenFileDialog(
|
||||||
|
callback,
|
||||||
|
nil,
|
||||||
|
CyberarmEngine::Window.instance.sdl_window,
|
||||||
|
nil,
|
||||||
|
0,
|
||||||
|
W3DHubLauncher::CACHE_PATH,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.choose_folder()
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.save_file()
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -3,11 +3,20 @@ module W3DHubLauncher
|
|||||||
BLACK_IMAGE = Gosu.render(64, 64, retro: true) { Gosu.draw_rect(0, 0, 32, 32, Gosu::Color::BLACK) }
|
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) }
|
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)
|
||||||
return get_image(path, retro: retro) if File.exist?(path)
|
raise RuntimeError, "Images may only be loaded from the main thread!" unless Thread.current == Thread.main
|
||||||
|
|
||||||
path = "#{ROOT_PATH}/media/default.png"
|
begin
|
||||||
return get_image(path, retro: retro) if File.exist?(path)
|
return get_image(path, retro: retro) if File.exist?(path)
|
||||||
|
rescue RuntimeError => e
|
||||||
|
pp e
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
return get_image(fallback_path, retro: retro) if File.exist?(fallback_path)
|
||||||
|
rescue RuntimeError => e
|
||||||
|
pp e
|
||||||
|
end
|
||||||
|
|
||||||
WHITE_IMAGE
|
WHITE_IMAGE
|
||||||
end
|
end
|
||||||
@@ -21,5 +30,33 @@ module W3DHubLauncher
|
|||||||
circle.draw(0, 0, 1, 1, 1, 0xff_ffffff, :multiply)
|
circle.draw(0, 0, 1, 1, 1, 0xff_ffffff, :multiply)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Downloads requested url and on success check if host element is still in the layout and call the block if so.
|
||||||
|
def remote_image(path, url:, element:, &block)
|
||||||
|
memcache_key = :"lock_net_download_#{element.style.tag}"
|
||||||
|
|
||||||
|
if !File.exist?(path) && MemCache[memcache_key].nil?
|
||||||
|
MemCache[memcache_key] = true
|
||||||
|
|
||||||
|
Worker::Api.download_url(url, path) do |result|
|
||||||
|
# ignore progress reports
|
||||||
|
next unless result.okay? && result.data == true
|
||||||
|
|
||||||
|
MemCache.delete(memcache_key)
|
||||||
|
# does the target element still exist in the layout?
|
||||||
|
e = find_element_by_tag(element.root, element.style.tag)
|
||||||
|
|
||||||
|
next unless e
|
||||||
|
|
||||||
|
if result.okay?
|
||||||
|
block.call(element, path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def on_main_thread(block)
|
||||||
|
window.add_to_queue(block)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
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
|
||||||
145
lib/ico.rb
Normal file
145
lib/ico.rb
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
require "stringio"
|
||||||
|
|
||||||
|
module W3DHubLauncher
|
||||||
|
class ICO
|
||||||
|
PNG_IDENT = "\211PNG\r\n\032\n".force_encoding("ASCII-8BIT").freeze
|
||||||
|
|
||||||
|
IconDirectory = Struct.new(:reserved, :type, :image_count)
|
||||||
|
|
||||||
|
IconDirectoryEntity = Struct.new(
|
||||||
|
:width,
|
||||||
|
:height,
|
||||||
|
:palette_size,
|
||||||
|
:reserved,
|
||||||
|
:color_planes,
|
||||||
|
:bit_depth,
|
||||||
|
:image_size,
|
||||||
|
:image_offset
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(file:)
|
||||||
|
@file = StringIO.new(File.binread(file))
|
||||||
|
|
||||||
|
@images = []
|
||||||
|
|
||||||
|
parse
|
||||||
|
end
|
||||||
|
|
||||||
|
def directory
|
||||||
|
@icon_directory
|
||||||
|
end
|
||||||
|
|
||||||
|
def images
|
||||||
|
@images
|
||||||
|
end
|
||||||
|
|
||||||
|
# SEE: https://en.wikipedia.org/wiki/ICO_(file_format)
|
||||||
|
def parse
|
||||||
|
# Parse IconDirectory
|
||||||
|
@icon_directory = IconDirectory.new(
|
||||||
|
read_u16, read_u16, read_u16
|
||||||
|
)
|
||||||
|
|
||||||
|
@icon_directory.image_count.times do
|
||||||
|
@images << IconDirectoryEntity.new(
|
||||||
|
read_u8,
|
||||||
|
read_u8,
|
||||||
|
read_u8,
|
||||||
|
read_u8,
|
||||||
|
read_u16,
|
||||||
|
read_u16,
|
||||||
|
read_u32,
|
||||||
|
read_u32
|
||||||
|
)
|
||||||
|
|
||||||
|
@images.last.width = 256 if @images.last.width == 0
|
||||||
|
@images.last.height = 256 if @images.last.height == 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_u8
|
||||||
|
@file.read(1).unpack1("C")
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_u16
|
||||||
|
@file.read(2).unpack1("v")
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_u32
|
||||||
|
@file.read(4).unpack1("V")
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_pngs
|
||||||
|
@images.select do |image|
|
||||||
|
image_png?(image)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_png?(image)
|
||||||
|
@file.pos = image.image_offset
|
||||||
|
buf = @file.read(8).unpack1("a*")
|
||||||
|
|
||||||
|
buf == PNG_IDENT
|
||||||
|
end
|
||||||
|
|
||||||
|
def select_bmps
|
||||||
|
@images.select do |image|
|
||||||
|
image_bmp?(image) && image.palette_size.zero? && image.bit_depth == 32
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_bmp?(image)
|
||||||
|
!image_png?(image)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_rgba32_blob(image)
|
||||||
|
@file.pos = image.image_offset
|
||||||
|
buf = StringIO.new(@file.read(image.image_size))
|
||||||
|
|
||||||
|
raise NotImplementedError "Cannot parse png based icons!" unless image_bmp?(image)
|
||||||
|
|
||||||
|
raise NotImplementedError "Cannot parse #{image.bit_depth}" unless image.bit_depth == 32
|
||||||
|
|
||||||
|
blob = "".force_encoding("ASCII-8BIT")
|
||||||
|
|
||||||
|
width = image.width
|
||||||
|
height = image.height - 1
|
||||||
|
|
||||||
|
image.height.times do |y|
|
||||||
|
image.width.times do |x|
|
||||||
|
buf.pos = ((height - y) * width + x + 10) * 4
|
||||||
|
|
||||||
|
blue = buf.read(1)
|
||||||
|
green = buf.read(1)
|
||||||
|
red = buf.read(1)
|
||||||
|
alpha = buf.read(1)
|
||||||
|
|
||||||
|
blob << red
|
||||||
|
blob << green
|
||||||
|
blob << blue
|
||||||
|
blob << alpha
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Gosu::Image.from_blob(image.width, image.height, blob)
|
||||||
|
ChunkyPNG::Canvas.from_rgba_stream(image.width, image.height, blob)
|
||||||
|
end
|
||||||
|
|
||||||
|
def image_data(image)
|
||||||
|
@file.pos = image.image_offset
|
||||||
|
StringIO.new(@file.read(image.image_size)).string
|
||||||
|
end
|
||||||
|
|
||||||
|
def save(image, filename)
|
||||||
|
puts filename
|
||||||
|
if image_bmp?(image)
|
||||||
|
puts :gosu_image
|
||||||
|
to_rgba32_blob(image).save(filename)
|
||||||
|
else
|
||||||
|
puts :png
|
||||||
|
File.open(filename, "wb") { |f| f.write(image_data(image)) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -13,48 +13,77 @@ module W3DHubLauncher
|
|||||||
stack(width: 1.0, fill: true, padding: PADDING) do
|
stack(width: 1.0, fill: true, padding: PADDING) do
|
||||||
stack(width: 1.0, fill: true, scroll: true) do
|
stack(width: 1.0, fill: true, scroll: true) do
|
||||||
title "Initial Setup"
|
title "Initial Setup"
|
||||||
caption "Please confirm launcher's default settings and make any desired tweaks.", font: FONT_REGULAR
|
caption "Please confirm launcher's default settings and make any desired adjustments. These can be changed in the launcher settings later.", font: FONT_REGULAR, margin_left: PADDING
|
||||||
|
|
||||||
flow(width: 1.0, height: 40, margin_top: PADDING) do
|
flow(width: 1.0, height: 40, margin_top: PADDING) do
|
||||||
tagline "Launcher data directory", height: 1.0, text_v_align: :center
|
tagline "Nickname", height: 1.0, text_v_align: :center
|
||||||
edit_line format("%s/.local/share/W3D Hub", Dir.home), fill: true
|
@nickname = edit_line Etc.getlogin, fill: true
|
||||||
button "Browse..."
|
|
||||||
end
|
end
|
||||||
inscription "Location where the launcher stores it's configuration files and cache interface data."
|
inscription "Nickname to use when joining servers.", margin_left: PADDING
|
||||||
|
|
||||||
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
||||||
tagline "Launcher package cache directory", height: 1.0, text_v_align: :center
|
tagline "Launcher package cache directory", height: 1.0, text_v_align: :center
|
||||||
edit_line format("%s/.local/share/W3D Hub/package-cache", Dir.home), fill: true
|
@launcher_package_cache_directory = edit_line DEFAULT_PACKAGE_CACHE_PATH, fill: true
|
||||||
button "Browse..."
|
button "Browse..."
|
||||||
end
|
end
|
||||||
inscription "Location where the launcher will download application packages."
|
inscription "Location where the launcher will download application packages.", margin_left: PADDING
|
||||||
|
|
||||||
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
||||||
tagline "Application installation directory", height: 1.0, text_v_align: :center
|
tagline "Application installation directory", height: 1.0, text_v_align: :center
|
||||||
edit_line format("%s/.local/share/W3D Hub/applications", Dir.home), fill: true
|
@application_installation_directory = edit_line DEFAULT_APPLICATIONS_PATH, fill: true
|
||||||
button "Browse..."
|
button "Browse..."
|
||||||
end
|
end
|
||||||
inscription "Location where the launcher will install new applications."
|
inscription "Location where the launcher will install new applications.", margin_left: PADDING
|
||||||
|
|
||||||
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
||||||
tagline "Wine context", height: 1.0, text_v_align: :center
|
tagline "Wine prefix path", height: 1.0, text_v_align: :center
|
||||||
edit_line "", fill: true
|
@wine_prefix_path = edit_line "", fill: true
|
||||||
button "Browse..."
|
button "Browse..."
|
||||||
end
|
end
|
||||||
inscription "Location of wine context to use. Leave blank to use default."
|
inscription "Location of wine prefix to use. Leave blank to use default.", margin_left: PADDING
|
||||||
|
|
||||||
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
||||||
tagline "Wine command", height: 1.0, text_v_align: :center
|
tagline "Wine command", height: 1.0, text_v_align: :center
|
||||||
edit_line "wine", fill: true
|
@wine_command = edit_line "wine", fill: true
|
||||||
button "Browse..."
|
button "Browse..."
|
||||||
end
|
end
|
||||||
inscription "Path to wine executable. Use `wine` for system installed wine."
|
inscription "Path to wine executable. Use <c=f80>wine</c> for system installed wine.", margin_left: PADDING
|
||||||
|
|
||||||
|
flow(width: 1.0, height: 40, margin_top: HALF_PADDING) do
|
||||||
|
tagline "Winetricks command", height: 1.0, text_v_align: :center
|
||||||
|
@winetricks_command = edit_line "winetricks", fill: true
|
||||||
|
button "Browse..."
|
||||||
|
end
|
||||||
|
inscription "Path to winetricks executable. Use <c=f80>winetricks</c> for system installed winetricks.", margin_left: PADDING
|
||||||
end
|
end
|
||||||
|
|
||||||
flow(width: 1.0, padding_top: PADDING) do
|
flow(width: 1.0, padding_top: PADDING) do
|
||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
button "Accept" do
|
button "Accept", **CTA_BUTTON_THEME do |btn|
|
||||||
|
btn.enabled = false
|
||||||
|
|
||||||
|
# initialize default settings
|
||||||
|
settings = Worker::Api::Settings.new({})
|
||||||
|
|
||||||
|
# update user preferences
|
||||||
|
settings.preferences.nickname = @nickname.value.strip
|
||||||
|
settings.preferences.launcher_package_cache_directory = @launcher_package_cache_directory.value.strip
|
||||||
|
settings.preferences.application_installation_directory = @application_installation_directory.value.strip
|
||||||
|
settings.preferences.wine_prefix_path = @wine_prefix_path.value.strip
|
||||||
|
settings.preferences.wine_command = @wine_command.value.strip
|
||||||
|
settings.preferences.winetricks_command = @winetricks_command.value.strip
|
||||||
|
|
||||||
|
Worker::Api.update_settings(settings) do |result|
|
||||||
|
if result.okay?
|
||||||
|
on_main_thread(proc {
|
||||||
|
MemCache[:settings] = settings
|
||||||
page(Page::Boot::StartUp)
|
page(Page::Boot::StartUp)
|
||||||
|
})
|
||||||
|
else
|
||||||
|
# FIXME: Somehow something went wrong... show an error message.
|
||||||
|
btn.enabled = true
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,75 +5,219 @@ module W3DHubLauncher
|
|||||||
include GuiExt
|
include GuiExt
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
|
@steps = [
|
||||||
|
method(:step_connectivity),
|
||||||
|
method(:step_launcher_update),
|
||||||
|
method(:step_server_list),
|
||||||
|
method(:step_refresh_account_data),
|
||||||
|
method(:step_fetch_applications),
|
||||||
|
method(:step_battlefield_control_established)
|
||||||
|
]
|
||||||
|
@initialization_acceptable = false
|
||||||
|
@step_count = @steps.size
|
||||||
|
|
||||||
stack(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: 8, background_nine_slice_color: 0x11_ffffff, padding: HALF_PADDING) do
|
stack(width: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: 8, background_nine_slice_color: 0x11_ffffff, padding: HALF_PADDING) do
|
||||||
banner "Establishing Battlefield Control", width: 1.0, text_align: :center
|
banner "Establishing Battlefield Control", width: 1.0, text_align: :center
|
||||||
tagline "Please stand by...", width: 1.0, text_align: :center
|
tagline "Please stand by...", width: 1.0, text_align: :center
|
||||||
end
|
end
|
||||||
|
|
||||||
stack(width: 1.0, fill: true, padding: PADDING) do
|
stack(width: 1.0, fill: true, padding: PADDING) do
|
||||||
stack(width: 1.0, fill: true, scroll: true) do
|
@initialization_container = stack(width: 1.0, fill: true, scroll: true) do
|
||||||
a = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
# a = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/checkmark.png"), height: 1.0, v_align: :center
|
# image safe_get_image("#{ROOT_PATH}/media/icons/checkmark.png"), height: 1.0, v_align: :center
|
||||||
tagline "DNS resolution", height: 1.0, text_v_align: :center
|
# tagline "DNS resolution", height: 1.0, text_v_align: :center
|
||||||
end
|
# end
|
||||||
after(500) do
|
# after(500) do
|
||||||
a.show
|
# a.show
|
||||||
end
|
# end
|
||||||
b = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
# b = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 1.0, v_align: :center, color: 0xff_ff8800
|
# image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 1.0, v_align: :center, color: 0xff_ff8800
|
||||||
tagline "Upstream backend", height: 1.0, text_v_align: :center
|
# tagline "Upstream backend", height: 1.0, text_v_align: :center
|
||||||
caption "Notice: Unreachable. Accounts will be unavailable.", height: 1.0, text_v_align: :center, color: 0xff_ff8800
|
# caption "Notice: Unreachable. Accounts will be unavailable.", height: 1.0, text_v_align: :center, color: 0xff_ff8800
|
||||||
end
|
# end
|
||||||
after(1000) do
|
# after(1000) do
|
||||||
b.show
|
# b.show
|
||||||
end
|
# end
|
||||||
c = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
# c = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/checkmark.png"), height: 1.0, v_align: :center
|
# image safe_get_image("#{ROOT_PATH}/media/icons/checkmark.png"), height: 1.0, v_align: :center
|
||||||
tagline "Alternate backend", height: 1.0, text_v_align: :center
|
# tagline "Alternate backend", height: 1.0, text_v_align: :center
|
||||||
end
|
# end
|
||||||
after(1500) do
|
# after(1500) do
|
||||||
c.show
|
# c.show
|
||||||
end
|
# end
|
||||||
d = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
# d = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 1.0, v_align: :center, color: 0xff_ff8800
|
# image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 1.0, v_align: :center, color: 0xff_ff8800
|
||||||
tagline "Refresh account session", height: 1.0, text_v_align: :center
|
# tagline "Refresh account session", height: 1.0, text_v_align: :center
|
||||||
caption "Notice: Upstream backend unavailable, session not refreshed.", height: 1.0, text_v_align: :center, color: 0xff_ff8800
|
# caption "Notice: Upstream backend unavailable, session not refreshed.", height: 1.0, text_v_align: :center, color: 0xff_ff8800
|
||||||
end
|
# end
|
||||||
after(2000) do
|
# after(2000) do
|
||||||
d.show
|
# d.show
|
||||||
end
|
# end
|
||||||
e = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
# e = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/checkmark.png"), height: 1.0, v_align: :center
|
# image safe_get_image("#{ROOT_PATH}/media/icons/checkmark.png"), height: 1.0, v_align: :center
|
||||||
tagline "Fetch game servers", height: 1.0, text_v_align: :center
|
# tagline "Fetch game servers", height: 1.0, text_v_align: :center
|
||||||
end
|
# end
|
||||||
after(2500) do
|
# after(2500) do
|
||||||
e.show
|
# e.show
|
||||||
end
|
# end
|
||||||
f = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
# f = flow(width: 1.0, height: HALF_PADDING + PADDING, visible: false) do
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/cross.png"), height: 1.0, v_align: :center, color: 0xff_ff0000
|
# image safe_get_image("#{ROOT_PATH}/media/icons/cross.png"), height: 1.0, v_align: :center, color: 0xff_ff0000
|
||||||
tagline "Fetch applications", height: 1.0, text_v_align: :center
|
# tagline "Fetch applications", height: 1.0, text_v_align: :center
|
||||||
caption "Fatal: Failed to retrieve applications list and no local cache exists. Cannot continue.", height: 1.0, text_v_align: :center, color: 0xff_ff0000
|
# caption "Fatal: Failed to retrieve applications list and no local cache exists. Cannot continue.", height: 1.0, text_v_align: :center, color: 0xff_ff0000
|
||||||
end
|
# end
|
||||||
after(3000) do
|
# after(3000) do
|
||||||
f.show
|
# puts "HELLO"
|
||||||
@progress_bar.type = :linear
|
# W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_applications }) do |result|
|
||||||
@progress_bar.value = 0.0
|
# # pp [:CALLBACK, result]
|
||||||
|
# File.write("applications.json", result.data)
|
||||||
|
# hash = JSON.parse(result.data)
|
||||||
|
# applications = hash["applications"]&.map { |app| W3DHubLauncher::Worker::Api::Application.new(app) } || []
|
||||||
|
# # pp applications.to_json
|
||||||
|
|
||||||
|
# MemCache[:applications] = applications
|
||||||
|
# end
|
||||||
|
|
||||||
|
# f.show
|
||||||
|
# @progress_bar.type = :linear
|
||||||
|
# @progress_bar.value = 0.0
|
||||||
|
# end
|
||||||
|
|
||||||
|
# after(35000) do
|
||||||
|
# parent.page_host.clear do
|
||||||
|
# banner "Battlefield control established".upcase, width: 1.0, height: 1.0, text_v_align: :center, text_align: :center
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# after(36000) do
|
||||||
|
# pop_state
|
||||||
|
# push_state(States::Interface)
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
|
|
||||||
after(3500) do
|
flow(width: 1.0, padding_top: PADDING) do
|
||||||
parent.page_host.clear do
|
@progress_bar = progress width: 1.0, fraction: 0.0
|
||||||
banner "Battlefield control established".upcase, width: 1.0, height: 1.0, text_v_align: :center, text_align: :center
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
after(3600) do
|
|
||||||
|
next_step
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_step
|
||||||
|
step = @steps.shift
|
||||||
|
step&.call
|
||||||
|
|
||||||
|
# pp (1.0 - (@steps.size / @step_count.to_f)), @steps.size, @step_count
|
||||||
|
@progress_bar.value = (1.0 - (@steps.size / @step_count.to_f))
|
||||||
|
|
||||||
|
|
||||||
|
if step.nil? && @initialization_acceptable
|
||||||
|
after(100) do
|
||||||
pop_state
|
pop_state
|
||||||
push_state(States::Interface)
|
push_state(States::Interface)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
flow(width: 1.0, padding_top: PADDING) do
|
def step_connectivity
|
||||||
@progress_bar = progress width: 1.0, type: :marquee
|
@initialization_container.append do
|
||||||
end
|
tagline "Checking uplink..."
|
||||||
|
# caption "Primary backend services <c=080>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
# caption "Upstream backend services <c=080>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
end
|
||||||
|
|
||||||
|
Worker::Api.dns_resolution do |result|
|
||||||
|
@initialization_container.append do
|
||||||
|
if result.okay?
|
||||||
|
caption "DNS Resolution <c=0f0>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
next_step
|
||||||
|
else
|
||||||
|
caption "DNS Resolution <c=f00>FAILED</c>", margin_left: LARGE_PADDING
|
||||||
|
caption "Failed to resolve: #{result.error}", color: 0xff_ff0000, margin_left: LARGE_PADDING * 2
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def step_launcher_update
|
||||||
|
@initialization_container.append do
|
||||||
|
tagline "Checking for updates..."
|
||||||
|
caption "Launcher version <c=0f0>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
end
|
||||||
|
|
||||||
|
after(1000) do
|
||||||
|
next_step
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def step_server_list
|
||||||
|
@initialization_container.append do
|
||||||
|
tagline "Requesting server listing..."
|
||||||
|
end
|
||||||
|
|
||||||
|
Worker::Api.servers do |result|
|
||||||
|
@initialization_container.append do
|
||||||
|
if result.okay?
|
||||||
|
MemCache[:servers] = JSON.parse(result.data).map { |s| Worker::Api::GameServer.new(s) }
|
||||||
|
|
||||||
|
caption "Received #{MemCache[:servers].size} servers <c=0f0>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
else
|
||||||
|
caption "Failed to retrieve server listing.", color: 0xff_ff8800, margin_left: LARGE_PADDING
|
||||||
|
end
|
||||||
|
|
||||||
|
next_step
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def step_refresh_account_data
|
||||||
|
@initialization_container.append do
|
||||||
|
tagline "Updating account information..."
|
||||||
|
caption "Refresh session <c=0f0>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
caption "Profile data <c=0f0>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
end
|
||||||
|
|
||||||
|
after(1000) do
|
||||||
|
next_step
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def step_fetch_applications
|
||||||
|
@initialization_container.append do
|
||||||
|
tagline "Requesting application listing..."
|
||||||
|
end
|
||||||
|
|
||||||
|
Worker::Api.applications do |result|
|
||||||
|
if result.okay?
|
||||||
|
File.write("applications.json", result.data)
|
||||||
|
hash = JSON.parse(result.data)
|
||||||
|
applications = hash["applications"]&.map { |app| W3DHubLauncher::Worker::Api::Application.new(app) } || []
|
||||||
|
|
||||||
|
MemCache[:applications] = applications
|
||||||
|
|
||||||
|
@initialization_container.append do
|
||||||
|
caption "Received #{MemCache[:applications].select(&:game?).size} applications <c=0f0>OK</c>", margin_left: LARGE_PADDING
|
||||||
|
end
|
||||||
|
|
||||||
|
next_step
|
||||||
|
else
|
||||||
|
@initialization_container.append do
|
||||||
|
caption "A fatal error occurred while getting application data.", margin_left: LARGE_PADDING, color: 0xaa_ff0000
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def step_battlefield_control_established
|
||||||
|
@initialization_acceptable = true
|
||||||
|
|
||||||
|
@initialization_container.append do
|
||||||
|
stack(fill: true)
|
||||||
|
caption "All required systems operational", color: 0xff_00ff00, width: 1.0, text_v_align: :center, text_align: :center
|
||||||
|
banner "Battlefield control established".upcase, width: 1.0, text_v_align: :center, text_align: :center
|
||||||
|
end
|
||||||
|
@progress_bar.hide
|
||||||
|
|
||||||
|
after(800) do
|
||||||
|
next_step
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ module W3DHubLauncher
|
|||||||
window.close
|
window.close
|
||||||
end
|
end
|
||||||
|
|
||||||
button "Accept" do
|
button "Accept", **CTA_BUTTON_THEME do
|
||||||
page(Page::Boot::InitialSetup)
|
page(Page::Boot::InitialSetup)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,77 +4,315 @@ module W3DHubLauncher
|
|||||||
include GuiExt
|
include GuiExt
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
|
@games = MemCache[:applications]&.select { |app| app.type.include?("game") } || []
|
||||||
|
|
||||||
|
@current_app = @games.first
|
||||||
|
@current_channel = @current_app&.channels&.first
|
||||||
|
|
||||||
|
@display_mode = :all_games
|
||||||
|
|
||||||
# game bar container
|
# game bar container
|
||||||
flow(width: 1.0, height: 60) do
|
flow(width: 1.0, height: 60) do
|
||||||
widget(width: 220, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY, hover: { background_nine_slice_color: ALPHA_BLACK }, active: { background_nine_slice_color: ALPHA_GRAY }) 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
|
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
|
link "ALL GAMES", text_size: 24, font: FONT_BLACK, height: 1.0, text_v_align: :center
|
||||||
end
|
end
|
||||||
|
w.subscribe(:clicked_left_mouse_button) do
|
||||||
|
populate_all_games
|
||||||
|
end
|
||||||
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
|
@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
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/apb.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0x88_5e5c64, border_thickness_bottom: 3, border_color_bottom: 0xff_3584e4, tip: "Red Alert: A Path Beyond"
|
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/ren.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: "Command & Conquer: Renegade"
|
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/tsr.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: "Tiberian Sun: Reborn"
|
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/woa.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: "Battle for Dune: War of Assassins"
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# game content container
|
# game content container
|
||||||
flow(width: 1.0, fill: true, margin_top: LARGE_PADDING) do
|
@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
|
@game_info_container = stack(width: 340, height: 1.0) do
|
||||||
# logo
|
|
||||||
image safe_get_image("/run/media/cyberarm/Storage/W3DHub/Launcher/package-cache/games/apb/logo.png.package"), width: 1.0, max_height: 124
|
|
||||||
|
|
||||||
# web links
|
|
||||||
stack(width: 1.0, fill: true, padding: 0, padding_top: LARGE_PADDING) do
|
|
||||||
link "Modifications", text_size: 24
|
|
||||||
link "Bug Tracker", text_size: 24
|
|
||||||
link "Player Statistics", text_size: 24
|
|
||||||
end
|
|
||||||
|
|
||||||
# launching ta game
|
|
||||||
caption "Game Version"
|
|
||||||
list_box items: [ "Release", "Open Testing" ], width: 1.0, margin_bottom: PADDING
|
|
||||||
flow(width: 1.0, height: 60) do
|
|
||||||
button "PLAY", 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"), image_height: 1.0, background_nine_slice: NINE_SLICE_SQUARE, **CTA_BUTTON_THEME
|
|
||||||
button safe_get_image("#{ROOT_PATH}/media/icons/gear.png"), image_height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED_RIGHT, **CTA_BUTTON_THEME
|
|
||||||
end
|
|
||||||
inscription "Version: 3.9.2.15", margin_top: PADDING
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# game events and news container
|
# game events and news container
|
||||||
stack(fill: true, height: 1.0, margin_left: LARGE_PADDING, scroll: true) do
|
stack(fill: true, height: 1.0, margin_left: PADDING, scroll: true) do
|
||||||
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
|
@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
|
||||||
image safe_get_image("#{ROOT_PATH}/media/background.png"), fill: true, aspect_ratio: 16.0 / 9.0
|
|
||||||
|
|
||||||
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
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# news container
|
# news container
|
||||||
flow(width: 1.0, margin_top: PADDING) do
|
@news_container = flow(width: 1.0) do
|
||||||
9.times do
|
end
|
||||||
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)
|
# "dynamically" adjust news item widths
|
||||||
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
|
@news_container.subscribe(:size_changed) do |event|
|
||||||
caption "NEWS", color: 0x88_ffffff
|
@news_container.children.each do |box|
|
||||||
tagline "A News Item Post A News Item Post"
|
box.style.width = 1.0 / news_item_width_ratio
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
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
|
||||||
|
|
||||||
|
# This shouldn't be possible, but you never know...
|
||||||
|
unless @current_app
|
||||||
|
@games_list_container.clear
|
||||||
|
@game_info_container.clear
|
||||||
|
|
||||||
|
@event_container.clear
|
||||||
|
@event_container.hide
|
||||||
|
|
||||||
|
@news_container.clear do
|
||||||
|
title "Something has gone wrong somewhere."
|
||||||
|
tagline "No games to display... ¯\\_(-|-)_/¯"
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
populate_game_news
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_games_list
|
||||||
|
@games_list_container.clear do
|
||||||
|
@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
|
||||||
|
remote_image("#{CACHE_PATH}/icon_#{game.id}.ico", url: "https://s3.w3d.cyberarm.dev/games/#{game.id}/#{game.id}.ico", element: btn) do |e, path|
|
||||||
|
Worker::Api.ico_to_png(ico_path: path, png_path: path.sub(".ico", ".png")) do |result|
|
||||||
|
e&.value = safe_get_image(result.data["path"]) if result.okay?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_game_info
|
||||||
|
application = MemCache[:settings].application_installed?(@current_app, @current_channel)
|
||||||
|
# pp application
|
||||||
|
|
||||||
|
@game_info_container.clear do
|
||||||
|
# logo
|
||||||
|
img = image safe_get_image("#{CACHE_PATH}/logo_#{@current_app.id}.png"), tag: :"image_logo_#{@current_app.id}", width: 1.0, max_height: 124
|
||||||
|
remote_image("#{CACHE_PATH}/logo_#{@current_app.id}.png", url: "https://s3.w3d.cyberarm.dev/games/#{@current_app.id}/logo.png", element: img) do |e, path|
|
||||||
|
e&.value = safe_get_image(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
# web links
|
||||||
|
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 do
|
||||||
|
SDL.OpenURL(link.uri)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# launching ta game
|
||||||
|
if (@current_app&.channels&.size || 0) > 1
|
||||||
|
caption "Game Version"
|
||||||
|
list_box(items: @current_app.channels.map(&:name), choose: @current_channel&.name, width: 1.0, margin_bottom: PADDING) do |item|
|
||||||
|
@current_channel = @current_app.channels.find { |c| c.name == item }
|
||||||
|
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 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", 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", tip: "Import existing application installation", fill: true, height: 1.0, **CTA_BUTTON_THEME do |btn|
|
||||||
|
dialog(Dialog::ImportApplication, application: @current_app, channel: @current_channel)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_game_event
|
||||||
|
app_events = MemCache[:"events_#{@current_app.id}"] || []
|
||||||
|
if app_events.empty?
|
||||||
|
@event_container.hide
|
||||||
|
else
|
||||||
|
@event_container.show
|
||||||
|
|
||||||
|
event = app_events.sort(&:start_time).last
|
||||||
|
|
||||||
|
@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
|
||||||
|
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
|
end
|
||||||
|
|||||||
@@ -4,45 +4,95 @@ module W3DHubLauncher
|
|||||||
include GuiExt
|
include GuiExt
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
|
@games_filter = []
|
||||||
|
|
||||||
# game bar container
|
# game bar container
|
||||||
flow(width: 1.0, height: 60) do
|
flow(width: 1.0, height: 60) do
|
||||||
flow(width: 220, height: 1.0, background_nine_slice: NINE_SLICE_ROUNDED, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: ALPHA_GRAY) 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
|
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
|
link "ALL SERVERS", text_size: 24, font: FONT_BLACK, height: 1.0, text_v_align: :center
|
||||||
end
|
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
|
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
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/apb.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0x88_5e5c64, border_thickness_bottom: 3, border_color_bottom: 0xff_3584e4, tip: "Red Alert: A Path Beyond"
|
MemCache[:applications].each do |app|
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/ren.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: "Command & Conquer: Renegade"
|
next unless app.game?
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/tsr.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: "Tiberian Sun: Reborn"
|
|
||||||
image safe_get_image("#{ROOT_PATH}/data/cache/woa.png"), height: 1.0, padding: HALF_PADDING, background_nine_slice: NINE_SLICE_ROUNDED_TOP, background_nine_slice_from_edge: NINE_SLICE_EDGE, background_nine_slice_color: 0, tip: "Battle for Dune: War of Assassins"
|
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!
|
||||||
|
else
|
||||||
|
@games_filter.clear
|
||||||
|
@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
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# game content container
|
# game content container
|
||||||
stack(width: 1.0, fill: true, margin_top: LARGE_PADDING, scroll: true) do
|
@servers_container = stack(width: 1.0, fill: true, margin_top: LARGE_PADDING, scroll: true) do
|
||||||
10.times do
|
end
|
||||||
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: ALPHA_GRAY, hover: { background_nine_slice_color: 0xff_5e5c64 } , active: { background_nine_slice_color: 0xaa_5e5c64 }) do
|
|
||||||
|
# FIXME: Remember and use last used filter
|
||||||
|
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)
|
||||||
|
# TODO: Update displayed server ping
|
||||||
|
end
|
||||||
|
|
||||||
|
def populate_server_list
|
||||||
|
@servers_container.clear do
|
||||||
|
(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: 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
|
# app icon container
|
||||||
image(safe_get_image("#{ROOT_PATH}/data/cache/apb.png"), tip: "Red Alert: A Path Beyond", 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)
|
||||||
# friend name and status container
|
|
||||||
|
# server name, region, and times container
|
||||||
stack(fill: true, height: 1.0, margin_left: HALF_PADDING) do
|
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
|
# server name
|
||||||
server_name = ["Really Long Server Name Goes Here", "[US][W3D Hub] APB Game Night", "[US][W3D Hub] APB Co-op 3.7", "Really Long Server Name Goes Here Really Long Server Name Goes Here"].sample
|
caption server.name, text_wrap: :none, tip: server.name
|
||||||
caption server_name, text_wrap: :none, tip: server_name
|
|
||||||
# server info
|
# server info
|
||||||
a = ["North America", "South America", "Europe", "Asia", "Antarctica", "Arctica", "Oceania"]
|
inscription "#{server.channel} • #{server.region} • #{server.time_elapsed} / #{server.match_remaining_time}", text_wrap: :none, margin_top: -HALF_PADDING
|
||||||
inscription "#{a.sample} • 13:52 / #{rand > 0.5 ? '∞' : '30:00'}", text_wrap: :none, margin_top: -HALF_PADDING
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# server map
|
# server map
|
||||||
stack(width: 256, height: 1.0, margin_left: HALF_PADDING) do
|
stack(width: 256, height: 1.0, margin_left: HALF_PADDING) do
|
||||||
stack(width: 1.0, fill: true, v_align: :center) do
|
stack(width: 1.0, fill: true, v_align: :center) do
|
||||||
server_map = ["RA_Under", "C&C Superduple Long Map Name Goes Here", "RA_NorthByNorthWest", "RA_HostileWatersParadox", "RA_PacificThreat"].sample
|
caption server.current_map, tip: server.current_map, text_wrap: :none, width: 1.0, text_align: :center
|
||||||
caption server_map, tip: server_map, text_wrap: :none, width: 1.0, text_align: :center
|
|
||||||
inscription "map", text_wrap: :none, width: 1.0, text_align: :center, margin_top: -HALF_PADDING
|
inscription "map", text_wrap: :none, width: 1.0, text_align: :center, margin_top: -HALF_PADDING
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -50,17 +100,22 @@ module W3DHubLauncher
|
|||||||
# server player count
|
# server player count
|
||||||
stack(width: 96, height: 1.0, margin_left: HALF_PADDING) do
|
stack(width: 96, height: 1.0, margin_left: HALF_PADDING) do
|
||||||
stack(width: 1.0, fill: true, v_align: :center) do
|
stack(width: 1.0, fill: true, v_align: :center) do
|
||||||
caption format("%d / %d", rand(60), rand(60..127)), width: 1.0, text_align: :center
|
caption format("%d / %d", server.player_count, server.max_players), width: 1.0, text_align: :center
|
||||||
inscription "players", text_wrap: :none, width: 1.0, text_align: :center, margin_top: -HALF_PADDING
|
inscription "players", text_wrap: :none, width: 1.0, text_align: :center, margin_top: -HALF_PADDING
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# server ping
|
# server ping
|
||||||
flow(width: 96, height: 1.0, margin_left: HALF_PADDING, margin_right: HALF_PADDING) do
|
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
|
stack(fill: true, height: 1.0, v_align: :center) do
|
||||||
caption rand > 0.85 ? "?" : format("%d ms", rand(16..360)), width: 1.0, text_align: :center
|
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
|
inscription "ping", text_wrap: :none, width: 1.0, text_align: :center, margin_top: -HALF_PADDING
|
||||||
end
|
end
|
||||||
stack(width: 8, height: rand(0.25..1.0), 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: [0xff_26a269, 0xff_e5a50a, 0xff_a51d2d, 0xff_3d3846].sample)
|
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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module W3DHubLauncher
|
|||||||
super
|
super
|
||||||
|
|
||||||
# root container - background image
|
# root container - background image
|
||||||
stack(width: 1.0, height: 1.0, background_image: safe_get_image("/run/media/cyberarm/Storage/W3DHub/Launcher/package-cache/games/apb/background.png.package"), background_image_mode: :fill) do
|
stack(width: 1.0, height: 1.0, background_image: safe_get_image("#{ROOT_PATH}/media/background.png"), background_image_mode: :fill) do
|
||||||
# root container - background image tint
|
# root container - background image tint
|
||||||
flow(width: 1.0, height: 1.0, background: 0xaa_000000) do
|
flow(width: 1.0, height: 1.0, background: 0xaa_000000) do
|
||||||
# content container
|
# content container
|
||||||
@@ -28,7 +28,18 @@ module W3DHubLauncher
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Worker::Api.load_settings do |result|
|
||||||
|
if result.okay?
|
||||||
|
on_main_thread( proc {
|
||||||
|
MemCache[:settings] = Worker::Api::Settings.new(JSON.parse(result.data))
|
||||||
|
page(Page::Boot::StartUp)
|
||||||
|
})
|
||||||
|
else
|
||||||
|
on_main_thread( proc {
|
||||||
page(Page::Boot::Terms)
|
page(Page::Boot::Terms)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
module W3DHubLauncher
|
module W3DHubLauncher
|
||||||
class States
|
class States
|
||||||
class Interface < W3DHubLauncher::State
|
class Interface < W3DHubLauncher::State
|
||||||
|
attr_reader :application_task_status_bar_container
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
super
|
super
|
||||||
|
|
||||||
# root container - background image
|
# root container - background image
|
||||||
stack(width: 1.0, height: 1.0, background_image: safe_get_image("/run/media/cyberarm/Storage/W3DHub/Launcher/package-cache/games/apb/background.png.package"), background_image_mode: :fill) do
|
stack(width: 1.0, height: 1.0, background_image: safe_get_image("#{ROOT_PATH}/media/background.png"), background_image_mode: :fill) do
|
||||||
# root container - background image tint
|
# root container - background image tint
|
||||||
flow(width: 1.0, height: 1.0, background: ALPHA_BLACK) do
|
stack(width: 1.0, height: 1.0, background: ALPHA_BLACK) do
|
||||||
# content container
|
# content container
|
||||||
stack(fill: true, height: 1.0, margin: PADDING, margin_right: LARGE_PADDING) do
|
stack(width: 1.0, fill: true, margin: PADDING) do
|
||||||
# header bar container
|
# 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
|
# 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|
|
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
|
menu(parent: btn) do
|
||||||
@@ -25,38 +27,46 @@ module W3DHubLauncher
|
|||||||
end.show
|
end.show
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# navigation bar container
|
||||||
stack(fill: true, height: 1.0) do
|
stack(fill: true, height: 1.0) do
|
||||||
|
# stack(fill: true)
|
||||||
|
|
||||||
|
# navigation container
|
||||||
|
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)
|
stack(fill: true)
|
||||||
flow(width: 1.0) do
|
image safe_get_image("#{ROOT_PATH}/media/icons/import.png"), height: 40, color: 0xff_bbbbbb, tip: "Downloads" do |img|
|
||||||
link("GAMES", text_v_align: :center, font: FONT_BLACK, margin_left: PADDING) { page(Page::Games) }
|
dialog(Dialog::Downloads)
|
||||||
link("SERVERS", text_v_align: :center, font: FONT_BLACK, margin_left: PADDING) { page(Page::ServerBrowser) }
|
end
|
||||||
stack(fill: true)
|
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/import.png"), height: 40, color: 0xff_bbbbbb, tip: "Downloads"
|
|
||||||
image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 40, color: 0xff_bbbbbb, tip: "Notifications"
|
image safe_get_image("#{ROOT_PATH}/media/icons/information.png"), height: 40, color: 0xff_bbbbbb, tip: "Notifications"
|
||||||
end
|
end
|
||||||
|
# application task status bar container
|
||||||
|
@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)", tag: :status_bar_title
|
||||||
stack(fill: true)
|
stack(fill: true)
|
||||||
|
para "Fetching manifests...", tag: :status_bar_label
|
||||||
|
end
|
||||||
|
progress(width: 1.0, fraction: 0.0, tag: :status_bar_progress)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# page content container
|
|
||||||
@page_host = stack(width: 1.0, fill: true) do
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# battleview/friends container
|
|
||||||
stack(width: 300, height: 1.0, margin: PADDING, margin_left: 0) do
|
|
||||||
# self account container
|
# self account container
|
||||||
flow(width: 1.0, height: 80) do
|
@account_container = flow(width: 400, height: 80, margin_left: LARGE_PADDING) do
|
||||||
# self avatar container
|
# self avatar container
|
||||||
stack(width: 80, height: 1.0, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png"))) do
|
stack(width: 80, height: 1.0, background_image: rounded_avatar(safe_get_image("#{ROOT_PATH}/media/default.png")), tag: :account_avatar) do |i|
|
||||||
|
i.subscribe(:clicked_left_mouse_button) do
|
||||||
|
dialog(Dialog::Account)
|
||||||
|
end
|
||||||
# self online state container
|
# self online state container
|
||||||
stack(width: 20, height: 20, v_align: :bottom, h_align: :right, background_image: safe_get_image("#{ROOT_PATH}/media/ui/circle_small.png"), background_image_color: 0xff_26a269)
|
stack(width: 20, height: 20, v_align: :bottom, h_align: :right, background_image: safe_get_image("#{ROOT_PATH}/media/ui/circle_small.png"), background_image_color: 0xff_26a269, tag: :account_online_state)
|
||||||
end
|
end
|
||||||
|
|
||||||
stack(fill: true, height: 1.0, margin_left: HALF_PADDING) do
|
stack(fill: true, height: 1.0, margin_left: HALF_PADDING) do
|
||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
# self name
|
# 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
|
# self set online state
|
||||||
link "Online ▼", text_size: 18 do |l|
|
link "Online ▼", text_size: 18 do |l|
|
||||||
menu(parent: l) do
|
menu(parent: l) do
|
||||||
@@ -70,9 +80,22 @@ module W3DHubLauncher
|
|||||||
flow(fill: true)
|
flow(fill: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# layout container
|
||||||
|
flow(width: 1.0, fill: true) do
|
||||||
|
# page host container
|
||||||
|
@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: 400, height: 1.0, margin_left: LARGE_PADDING) do
|
||||||
# friend management container
|
# friend management container
|
||||||
flow(width: 1.0, height: 60, margin_top: PADDING) do
|
flow(width: 1.0, height: 60) do
|
||||||
flow(width: 1.0, v_align: :center) do
|
flow(width: 1.0, v_align: :center) do
|
||||||
button safe_get_image("#{ROOT_PATH}/media/icons/singleplayer.png"), image_height: 1.0
|
button safe_get_image("#{ROOT_PATH}/media/icons/singleplayer.png"), image_height: 1.0
|
||||||
button safe_get_image("#{ROOT_PATH}/media/icons/gear.png"), image_height: 1.0, margin_left: HALF_PADDING
|
button safe_get_image("#{ROOT_PATH}/media/icons/gear.png"), image_height: 1.0, margin_left: HALF_PADDING
|
||||||
@@ -84,25 +107,27 @@ module W3DHubLauncher
|
|||||||
stack(width: 1.0, fill: true, margin_top: LARGE_PADDING, scroll: true) do
|
stack(width: 1.0, fill: true, margin_top: LARGE_PADDING, scroll: true) do
|
||||||
50.times do |i|
|
50.times do |i|
|
||||||
# friend container
|
# 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
|
w.subscribe(:clicked_left_mouse_button) do
|
||||||
puts "HELLO THERE"
|
puts "HELLO THERE"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
# friend avatar container
|
# 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)
|
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
|
end
|
||||||
# friend name and status container
|
# friend name and status container
|
||||||
stack(fill: true, height: 1.0, margin_left: HALF_PADDING, margin_right: HALF_PADDING) do
|
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
|
caption ["Silverlight", "PXD2000", "Alstar", "SteelGhost", "FRAYDO"].sample, text_wrap: :none
|
||||||
inscription "RA_Under • 13:52", text_wrap: :none, margin_top: -HALF_PADDING
|
inscription "RA_Under • 13:52", text_wrap: :none, margin_top: -HALF_PADDING
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
# friend active application container
|
# 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
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -110,8 +135,231 @@ module W3DHubLauncher
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
hide_server_details_panel
|
||||||
|
|
||||||
page(Page::Games)
|
page(Page::Games)
|
||||||
end
|
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: true,
|
||||||
|
text_border_size: 1,
|
||||||
|
text_border_color: 0xaa_000000,
|
||||||
|
text_shadow: false,
|
||||||
|
width: 1.0,
|
||||||
|
height: 1.0,
|
||||||
|
padding_bottom: HALF_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
|
||||||
|
|
||||||
|
@battleview_container.toggle if id == Gosu::KB_F8
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
35
lib/theme.rb
35
lib/theme.rb
@@ -2,8 +2,10 @@ module W3DHubLauncher
|
|||||||
LARGE_PADDING = 40
|
LARGE_PADDING = 40
|
||||||
PADDING = 20
|
PADDING = 20
|
||||||
HALF_PADDING = 10
|
HALF_PADDING = 10
|
||||||
|
|
||||||
ALPHA_BLACK = 0x88_000000
|
ALPHA_BLACK = 0x88_000000
|
||||||
ALPHA_GRAY = 0x88_5e5c64
|
ALPHA_GRAY = 0x88_5e5c64
|
||||||
|
TRANSLUCENT_GRAY = 0xaa_3d3846
|
||||||
|
|
||||||
FONT_LIGHT = "#{ROOT_PATH}/media/fonts/NotoSans-Light.ttf"
|
FONT_LIGHT = "#{ROOT_PATH}/media/fonts/NotoSans-Light.ttf"
|
||||||
FONT_REGULAR = "#{ROOT_PATH}/media/fonts/NotoSans-Regular.ttf"
|
FONT_REGULAR = "#{ROOT_PATH}/media/fonts/NotoSans-Regular.ttf"
|
||||||
@@ -77,6 +79,9 @@ module W3DHubLauncher
|
|||||||
text_shadow: false,
|
text_shadow: false,
|
||||||
color: 0xff_ffffff,
|
color: 0xff_ffffff,
|
||||||
background: 0,#x88_5e5c64,
|
background: 0,#x88_5e5c64,
|
||||||
|
padding_left: HALF_PADDING,
|
||||||
|
padding_right: HALF_PADDING,
|
||||||
|
border_color: 0,
|
||||||
background_nine_slice: NINE_SLICE_ROUNDED,
|
background_nine_slice: NINE_SLICE_ROUNDED,
|
||||||
background_nine_slice_from_edge: NINE_SLICE_EDGE,
|
background_nine_slice_from_edge: NINE_SLICE_EDGE,
|
||||||
background_nine_slice_mode: :stretched,
|
background_nine_slice_mode: :stretched,
|
||||||
@@ -91,6 +96,11 @@ module W3DHubLauncher
|
|||||||
color: 0x88_ffffff,
|
color: 0x88_ffffff,
|
||||||
background: 0,
|
background: 0,
|
||||||
background_nine_slice_color: 0xaa_5e5c64
|
background_nine_slice_color: 0xaa_5e5c64
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
color: 0x88_c0bfbc,
|
||||||
|
background: 0,
|
||||||
|
background_nine_slice_color: 0xaa_77767b
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
EditLine: {
|
EditLine: {
|
||||||
@@ -147,6 +157,31 @@ module W3DHubLauncher
|
|||||||
background_nine_slice_mode: :stretched,
|
background_nine_slice_mode: :stretched,
|
||||||
background_nine_slice_color: 0xcc_000000,
|
background_nine_slice_color: 0xcc_000000,
|
||||||
border_thickness: 0
|
border_thickness: 0
|
||||||
|
},
|
||||||
|
all_button: {
|
||||||
|
background_nine_slice: NINE_SLICE_ROUNDED,
|
||||||
|
background_nine_slice_from_edge: NINE_SLICE_EDGE,
|
||||||
|
background_nine_slice_color: ALPHA_GRAY,
|
||||||
|
hover: {
|
||||||
|
background_nine_slice_color: ALPHA_BLACK
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
background_nine_slice_color: ALPHA_GRAY
|
||||||
|
}
|
||||||
|
},
|
||||||
|
app_icon_button: {
|
||||||
|
height: 1.0,
|
||||||
|
image_height: 1.0,
|
||||||
|
padding: HALF_PADDING,
|
||||||
|
border_thickness_bottom: 3,
|
||||||
|
background_nine_slice: NINE_SLICE_ROUNDED_TOP,
|
||||||
|
background_nine_slice_from_edge: NINE_SLICE_EDGE,
|
||||||
|
background_nine_slice_color: 0,
|
||||||
|
disabled: { # fake disabled: styling the "selected" app uses
|
||||||
|
color: 0xff_ffffff,
|
||||||
|
background_nine_slice_color: 0x88_5e5c64,
|
||||||
|
border_color_bottom: 0xff_3584e4
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,12 +4,34 @@ module W3DHubLauncher
|
|||||||
self.show_cursor = true
|
self.show_cursor = true
|
||||||
self.caption = format("%s | v%s (%s)", NAME, VERSION, VERSION_NAME) # "Cyberarm's W3D Hub Linux Launcher | v2.0.0 alpha"
|
self.caption = format("%s | v%s (%s)", NAME, VERSION, VERSION_NAME) # "Cyberarm's W3D Hub Linux Launcher | v2.0.0 alpha"
|
||||||
|
|
||||||
# push_state(States::Boot)
|
@main_thread_queue = []
|
||||||
push_state(States::Interface)
|
|
||||||
|
CyberarmEngine::EventBus.subscribe("game_server_pings", self) do |payload|
|
||||||
|
MemCache[:game_server_pings] = payload
|
||||||
|
end
|
||||||
|
|
||||||
|
push_state(States::Boot)
|
||||||
|
# push_state(States::Interface)
|
||||||
end
|
end
|
||||||
|
|
||||||
def needs_redraw?
|
def needs_redraw?
|
||||||
states.any?(&:needs_repaint?)
|
states.any?(&:needs_repaint?)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
while(block = @main_thread_queue.shift)
|
||||||
|
block.call
|
||||||
|
end
|
||||||
|
|
||||||
|
WORKER.service
|
||||||
|
|
||||||
|
super
|
||||||
|
|
||||||
|
sleep 0.001
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_to_queue(block)
|
||||||
|
@main_thread_queue << block
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
465
lib/worker.rb
465
lib/worker.rb
@@ -1,42 +1,78 @@
|
|||||||
module W3DHubLauncher
|
module W3DHubLauncher
|
||||||
class Worker
|
class Worker
|
||||||
Response = Data.define(:status, :request_id, :data)
|
DEFAULT_HEADERS = [
|
||||||
|
["user-agent", W3DHubLauncher::USER_AGENT]
|
||||||
|
].freeze
|
||||||
|
DEFAULT_NETWORK_TIMEOUT = 30
|
||||||
|
RESPONSE_SEPARATOR = "\04".freeze
|
||||||
|
|
||||||
|
Response = Data.define(:status, :request_id, :result)
|
||||||
|
|
||||||
|
attr_reader :w3dhub_api, :settings
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect
|
||||||
|
puts :connect
|
||||||
|
|
||||||
|
@buffer = StringIO.new
|
||||||
|
@socket = UNIXSocket.new(IPC_PATH)
|
||||||
|
end
|
||||||
|
|
||||||
|
def connected?
|
||||||
|
@socket && !@socket.closed?
|
||||||
|
end
|
||||||
|
|
||||||
|
def listen
|
||||||
|
init_server
|
||||||
|
end
|
||||||
|
|
||||||
|
def init_server
|
||||||
@threads = []
|
@threads = []
|
||||||
@requests = []
|
@requests = []
|
||||||
|
@tasks = []
|
||||||
|
|
||||||
# next available request_id to assign incoming requests
|
@client = nil
|
||||||
@request_id = 0
|
|
||||||
|
@settings = nil
|
||||||
|
@game_servers = []
|
||||||
|
@host_pings = {}
|
||||||
|
|
||||||
# listen for requests from frontend
|
|
||||||
listener = Thread.new { listen }
|
|
||||||
# connect to and monitor GSH web service
|
# connect to and monitor GSH web service
|
||||||
@threads << Thread.new { game_server_hub_websocket }
|
@threads << Thread.new { game_server_hub_websocket }
|
||||||
# connect to and monitor Backend web service
|
# connect to and monitor Backend web service
|
||||||
@threads << Thread.new { backend_websocket }
|
@threads << Thread.new { backend_websocket }
|
||||||
|
# poke game servers to ascertain their ping relative to the local machine
|
||||||
|
@threads << Thread.new { ping_game_servers }
|
||||||
|
|
||||||
@w3dhub_api = W3DHubLauncher::W3DHubApi.new
|
@w3dhub_api = W3DHubLauncher::W3DHubApi.new
|
||||||
|
|
||||||
listener.join
|
Async do |task|
|
||||||
end
|
UNIXServer.open(IPC_PATH) do |server|
|
||||||
|
while(socket = server.accept)
|
||||||
|
@client = socket
|
||||||
|
|
||||||
def listen
|
task.async do
|
||||||
loop do
|
while(data = socket.gets)
|
||||||
query = Ractor.receive
|
json = JSON.parse(data)
|
||||||
pp query
|
query = Request::Query.new(type: json["type"].to_sym, request_id: json["request_id"], data: json["data"])
|
||||||
|
|
||||||
case query.type
|
# pp [:server_incoming, data, query]
|
||||||
when Request::FETCH_URL
|
|
||||||
when Request::DOWNLOAD_URL
|
if respond_to?(query.type)
|
||||||
when Request::W3DHUB_API_CALL
|
response = send(query.type, query)
|
||||||
Async do
|
# pp [:server_to_client, response]
|
||||||
result = @w3dhub_api.send(query.data[:call], *(query.data[:arguments] || []))
|
payload = { status: response.status, request_id: response.request_id, data: response.result.data, error: response.result.error }.to_json
|
||||||
response = Response.new(result.okay? ? Request::STATUS_COMPLETE : Request::STATUS_ERROR, query.request_id, result)
|
socket.write(payload)
|
||||||
Ractor.main.send(response)
|
socket.write(RESPONSE_SEPARATOR)
|
||||||
|
socket.flush
|
||||||
end
|
end
|
||||||
else
|
end
|
||||||
raise "UNKNOWN REQUEST"
|
end
|
||||||
|
end
|
||||||
|
ensure # manually delete "socket"
|
||||||
|
File.delete(IPC_PATH)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -46,5 +82,392 @@ module W3DHubLauncher
|
|||||||
|
|
||||||
def backend_websocket
|
def backend_websocket
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ping_game_servers
|
||||||
|
socket = Socket.open(Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_ICMP)
|
||||||
|
socket.setsockopt(:SOCKET, :TIMESTAMP, true)
|
||||||
|
|
||||||
|
Async do |task|
|
||||||
|
while true
|
||||||
|
echo_requests = {}
|
||||||
|
|
||||||
|
# Send pings to each unique server host
|
||||||
|
@game_servers.map(&:address).uniq.each do |server_address|
|
||||||
|
address = Socket.sockaddr_in(0, server_address)
|
||||||
|
sequence_id = Digest::SHA256.hexdigest("#{server_address}-#{Time.now.iso8601}")
|
||||||
|
|
||||||
|
# ICMP Echo Request, ICMP Code, <checksum>, <identifier>, <sequence number>, Data
|
||||||
|
msg = [8, 0, 0, 0, sequence_id].flatten.pack("C2n2A64")
|
||||||
|
echo_requests[sequence_id] = { send_time: Time.now, address: server_address, replied: false }
|
||||||
|
socket.send(msg, 0, address)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Receive replies until timeout
|
||||||
|
task.with_timeout(3) do
|
||||||
|
while(echo_requests.values.any? { |v| v[:replied] == false })
|
||||||
|
response, sender_address, flags, *controls = socket.recvmsg(256)
|
||||||
|
_type, _code, _identifier, _sequence, data = response.unpack("C2n2A64")
|
||||||
|
|
||||||
|
if (request = echo_requests[data])# && request[:address] == sender_address.
|
||||||
|
request[:replied] = true
|
||||||
|
round_trip_time = ((controls.last.timestamp - request[:send_time]) * 1000.0).round
|
||||||
|
@host_pings[request[:address]] = round_trip_time
|
||||||
|
|
||||||
|
# puts "#{request[:address]}: #{round_trip_time}ms"
|
||||||
|
# else
|
||||||
|
# # packet not for us, or it got mangled in transit.
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Async::TimeoutError
|
||||||
|
# puts "Timed out waiting for: #{echo_requests.values.select { |v| v[:replied] == false }.map { |v| v[:address] }.join(', ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
broadcast(:game_server_pings, data: @host_pings)
|
||||||
|
|
||||||
|
sleep 5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ensure
|
||||||
|
socket&.close
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast(type, data:)
|
||||||
|
payload = { type: :eventbus, request_id: -1, data: { type: type, data: data } }.to_json
|
||||||
|
|
||||||
|
@client&.write(payload)
|
||||||
|
@client&.write(RESPONSE_SEPARATOR)
|
||||||
|
@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]
|
||||||
|
payload = { type: query.type, request_id: query.request_id, data: query.data }.to_json
|
||||||
|
|
||||||
|
if respond_to?(query.type)
|
||||||
|
@socket.puts(payload)
|
||||||
|
else
|
||||||
|
raise "UNKNOWN REQUEST: #{query}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def service
|
||||||
|
data = @socket.read_nonblock(1_048_576) # 1 MB
|
||||||
|
@buffer << data
|
||||||
|
|
||||||
|
@buffer.rewind
|
||||||
|
payloads = @buffer.readlines(RESPONSE_SEPARATOR, chomp: true)
|
||||||
|
@buffer.reopen(@buffer.string[@buffer.pos..@buffer.length])
|
||||||
|
@buffer.pos = @buffer.length
|
||||||
|
|
||||||
|
payloads.each do |payload|
|
||||||
|
# pp [:CLIENT, data]
|
||||||
|
json = JSON.parse(payload)
|
||||||
|
request = W3DHubLauncher::Worker::Request.requests.find { |r| r.request_id == json["request_id"] }
|
||||||
|
|
||||||
|
# pp [json, request]
|
||||||
|
if request.nil? && json["request_id"] && json["type"] == "eventbus"
|
||||||
|
CyberarmEngine::Window.instance&.add_to_queue(proc {
|
||||||
|
CyberarmEngine::EventBus.publish(json["data"]["type"], json["data"]["data"])
|
||||||
|
})
|
||||||
|
else
|
||||||
|
CyberarmEngine::Window.instance&.add_to_queue(proc {
|
||||||
|
request.handle_event(
|
||||||
|
json["status"],
|
||||||
|
CyberarmEngine::Result.new(data: json["data"], error: json["error"])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue JSON::ParserError => e
|
||||||
|
puts "This should never happen!?!?!"
|
||||||
|
pp data, payload
|
||||||
|
end
|
||||||
|
|
||||||
|
rescue Errno::EWOULDBLOCK
|
||||||
|
end
|
||||||
|
|
||||||
|
def deliver_response(result, query)
|
||||||
|
response = Response.new(result.okay? ? Request::STATUS_COMPLETE : Request::STATUS_ERROR, query.request_id, result)
|
||||||
|
# pp response
|
||||||
|
|
||||||
|
response
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_directory(path)
|
||||||
|
pathname = Pathname.new(path)
|
||||||
|
|
||||||
|
unless Dir.exist?(pathname.dirname)
|
||||||
|
FileUtils.mkdir_p(pathname.dirname)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
#
|
||||||
|
# ------------ query / request handlers ------------
|
||||||
|
#
|
||||||
|
def fetch_url(query)
|
||||||
|
result = CyberarmEngine::Result.new
|
||||||
|
|
||||||
|
method = query.data["method"]
|
||||||
|
url = query.data["url"]
|
||||||
|
headers = query.data["headers"] || DEFAULT_HEADERS
|
||||||
|
body = query.data["body"]
|
||||||
|
|
||||||
|
Sync do |task|
|
||||||
|
task.with_timeout(DEFAULT_NETWORK_TIMEOUT) do
|
||||||
|
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
||||||
|
if response.success?
|
||||||
|
result.data = response.read
|
||||||
|
end
|
||||||
|
# rescue StandardError => e
|
||||||
|
# result.error = e
|
||||||
|
end
|
||||||
|
# rescue Async::TimeoutError
|
||||||
|
# result.error = e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
deliver_response(result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def download_url(query)
|
||||||
|
result = CyberarmEngine::Result.new
|
||||||
|
|
||||||
|
method = query.data["method"]
|
||||||
|
url = query.data["url"]
|
||||||
|
path = query.data["path"]
|
||||||
|
headers = query.data["headers"] || DEFAULT_HEADERS
|
||||||
|
body = query.data["body"]
|
||||||
|
|
||||||
|
create_directory(path)
|
||||||
|
|
||||||
|
Sync do |task|
|
||||||
|
task.with_timeout(DEFAULT_NETWORK_TIMEOUT) do
|
||||||
|
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
||||||
|
if response.success?
|
||||||
|
content_length = response.headers["content-length"] || 0
|
||||||
|
|
||||||
|
total_downloaded_bytes = 0
|
||||||
|
File.open(path, "wb") do |file|
|
||||||
|
response.each do |chunk|
|
||||||
|
file.write(chunk)
|
||||||
|
downloaded_bytes = chunk.length
|
||||||
|
total_downloaded_bytes += downloaded_bytes
|
||||||
|
|
||||||
|
progress_result = CyberarmEngine::Result.new(data: {
|
||||||
|
downloaded_bytes: downloaded_bytes,
|
||||||
|
total_downloaded_bytes: total_downloaded_bytes,
|
||||||
|
content_length: content_length
|
||||||
|
})
|
||||||
|
# FIXME: Send intermediate response to requester
|
||||||
|
# send(query, Response.new(Request::STATUS_IN_PROGRESS, query.request_id progress_result))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
result.data = true
|
||||||
|
end
|
||||||
|
# rescue StandardError => e
|
||||||
|
# result.error = e
|
||||||
|
end
|
||||||
|
# rescue Async::TimeoutError
|
||||||
|
# result.error = e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
deliver_response(result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ico_to_png(query)
|
||||||
|
result = CyberarmEngine::Result.new
|
||||||
|
|
||||||
|
begin
|
||||||
|
ico = ICO.new(file: query.data["ico_path"])
|
||||||
|
ico.save(ico.images.max_by(&:width), query.data["png_path"])
|
||||||
|
result.data = { path: query.data["png_path"] }
|
||||||
|
rescue => e
|
||||||
|
result.error = e
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
domains = [
|
||||||
|
"w3dhub-api.w3d.cyberarm.dev",
|
||||||
|
"s3.w3d.cyberarm.dev",
|
||||||
|
"secure.w3dhub.com"
|
||||||
|
]
|
||||||
|
|
||||||
|
domains.each do |domain|
|
||||||
|
Resolv.getaddress(domain)
|
||||||
|
rescue StandardError => e
|
||||||
|
result.error = "Failed to resolve: #{domain}"
|
||||||
|
end
|
||||||
|
|
||||||
|
result.data = true if result.error.nil?
|
||||||
|
|
||||||
|
deliver_response(result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def w3dhub_api_call(query)
|
||||||
|
result = @w3dhub_api.send(query.data["call"], *(query.data["arguments"] || []))
|
||||||
|
|
||||||
|
deliver_response(result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_settings(query)
|
||||||
|
result = CyberarmEngine::Result.new
|
||||||
|
|
||||||
|
path = "#{W3DHubLauncher::CONFIG_PATH}/settings.json"
|
||||||
|
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.")
|
||||||
|
end
|
||||||
|
rescue => e
|
||||||
|
result.error = e
|
||||||
|
end
|
||||||
|
|
||||||
|
deliver_response(result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_settings(query)
|
||||||
|
result = CyberarmEngine::Result.new
|
||||||
|
|
||||||
|
begin
|
||||||
|
@settings = Api::Settings.new(JSON.parse(query.data))
|
||||||
|
FileUtils.mkdir_p(W3DHubLauncher::CONFIG_PATH)
|
||||||
|
File.write("#{W3DHubLauncher::CONFIG_PATH}/settings.json", query.data)
|
||||||
|
|
||||||
|
result.data = query.data
|
||||||
|
rescue => e
|
||||||
|
result.error = e
|
||||||
|
end
|
||||||
|
|
||||||
|
deliver_response(result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def servers(query)
|
||||||
|
hash = {
|
||||||
|
"method" => "get",
|
||||||
|
"url" => "https://gsh.w3d.cyberarm.dev/listings/getAll/v2?statusLevel=2",
|
||||||
|
"headers" => DEFAULT_HEADERS,
|
||||||
|
"body" => query.data["body"]
|
||||||
|
}
|
||||||
|
|
||||||
|
# inject data into query
|
||||||
|
query = Request::Query.new(query.type, query.request_id, hash)
|
||||||
|
|
||||||
|
response = fetch_url(query)
|
||||||
|
|
||||||
|
if response.result.okay?
|
||||||
|
@game_servers = JSON.parse(response.result.data).map { |server| Worker::Api::GameServer.new(server) }
|
||||||
|
end
|
||||||
|
|
||||||
|
deliver_response(response.result, query)
|
||||||
|
end
|
||||||
|
|
||||||
|
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 task_update_application(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def task_repair_application(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def task_move_application(query)
|
||||||
|
end
|
||||||
|
|
||||||
|
def task_uninstall_application(query)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,78 +1,104 @@
|
|||||||
# Helper for launcher frontend to safely communicate with ractor (prevent deadlocks and concurrent access errors)
|
# Helper for launcher frontend to safely communicate with launcher backend
|
||||||
|
|
||||||
module W3DHubLauncher
|
module W3DHubLauncher
|
||||||
class Worker
|
class Worker
|
||||||
class Api
|
class Api
|
||||||
Request = Data.define(:coming_soon)
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@requests = []
|
|
||||||
end
|
|
||||||
|
|
||||||
# downloads requested resource, returns raw string
|
# downloads requested resource, returns raw string
|
||||||
def fetch_url
|
def self.fetch_url
|
||||||
end
|
end
|
||||||
|
|
||||||
# downloads requested resource, periodically reporting progress until completion, returning path for file on disk
|
# downloads requested resource, periodically reporting progress until completion, returning path for file on disk
|
||||||
def download_url
|
def self.download_url(url, path, method = :get, headers = nil, body = nil, &block)
|
||||||
|
Worker::Request.new(:download_url, { url: url, path: path, method: method, headers: headers, body: body }, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.ico_to_png(ico_path:, png_path:, &block)
|
||||||
|
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
|
end
|
||||||
|
|
||||||
# returns user account data
|
# returns user account data
|
||||||
#
|
#
|
||||||
# automatically handles signing in / refreshing token (DOES NOT remove account data if failed to refresh token due to network timeout)
|
# automatically handles signing in / refreshing token (DOES NOT remove account data if failed to refresh token due to network timeout)
|
||||||
def account
|
def self.account
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns launcher settings
|
# returns launcher settings
|
||||||
def settings
|
def self.settings
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns raw json of launcher settings
|
||||||
|
def self.load_settings(&block)
|
||||||
|
Worker::Request.new(:load_settings, "", &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# write updated launcher settings
|
# write updated launcher settings
|
||||||
def update_settings
|
def self.update_settings(settings, &block)
|
||||||
|
Worker::Request.new(:update_settings, settings.to_json, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns list of available applications
|
# returns list of available applications
|
||||||
#
|
#
|
||||||
# if updated list is requested, return cached version immediately and then the updated list later.
|
# if updated list is requested, return cached version immediately and then the updated list later.
|
||||||
def applications
|
def self.applications(&block)
|
||||||
|
W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_applications }, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns current list of servers as reported from GSH / cache
|
# returns current list of servers as reported from GSH / cache
|
||||||
def servers
|
def self.servers(&block)
|
||||||
|
Worker::Request.new(:servers, "", &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# returns news for application
|
# returns news for application
|
||||||
def news
|
def self.news(category = "launcher-home", &block)
|
||||||
|
W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_news, arguments: [category] }, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns news for application
|
||||||
|
def self.events(category, &block)
|
||||||
|
W3DHubLauncher::Worker::Request.new(:w3dhub_api_call, { call: :fetch_events, arguments: [category] }, &block)
|
||||||
end
|
end
|
||||||
|
|
||||||
# request installation of application
|
# request installation of application
|
||||||
#
|
#
|
||||||
# periodically reports progress until completion
|
# periodically reports progress until completion
|
||||||
def install_application
|
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
|
end
|
||||||
|
|
||||||
# request update of application
|
# request update of application
|
||||||
#
|
#
|
||||||
# periodically reports progress until completion
|
# periodically reports progress until completion
|
||||||
def update_application
|
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
|
end
|
||||||
|
|
||||||
# request repair of application
|
# request repair of application
|
||||||
#
|
#
|
||||||
# periodically reports progress until completion
|
# periodically reports progress until completion
|
||||||
def repair_application
|
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
|
end
|
||||||
|
|
||||||
# request relocation of application
|
# request relocation of application
|
||||||
#
|
#
|
||||||
# periodically reports progress until completion
|
# periodically reports progress until completion
|
||||||
def move_application
|
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
|
end
|
||||||
|
|
||||||
# request removal of application
|
# request removal of application
|
||||||
#
|
#
|
||||||
# periodically reports progress until completion
|
# periodically reports progress until completion
|
||||||
def uninstall_application
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
133
lib/worker/api/application.rb
Normal file
133
lib/worker/api/application.rb
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
module W3DHubLauncher
|
||||||
|
class Worker
|
||||||
|
class Api
|
||||||
|
class Application
|
||||||
|
attr_reader :id, :category, :name, :type, :channels, :extended_data, :web_links
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
# pp data
|
||||||
|
|
||||||
|
@id = data["id"]
|
||||||
|
@category = data["category"]
|
||||||
|
@name = data["name"]
|
||||||
|
@type = data["type"]
|
||||||
|
|
||||||
|
@channels = data["channels"]&.map { |c| Channel.new(c, self) } || []
|
||||||
|
|
||||||
|
@extended_data = data["extended-data"]&.map { |d| ExtendedData.new(d) } || nil
|
||||||
|
@web_links = data["web-links"]&.map { |d| WebLink.new(d) } || nil
|
||||||
|
|
||||||
|
@servicable = extended_data("application.isservicable", "true").strip.downcase == "true"
|
||||||
|
end
|
||||||
|
|
||||||
|
def extended_data(key, default)
|
||||||
|
v = @extended_data&.find { |d| d.name == key }&.value
|
||||||
|
return default if v.nil?
|
||||||
|
|
||||||
|
v
|
||||||
|
end
|
||||||
|
|
||||||
|
# libgosu friendly color
|
||||||
|
def color
|
||||||
|
"ff_#{extended_data("colour","#000000").sub("#", "")}".to_i(16)
|
||||||
|
end
|
||||||
|
|
||||||
|
def game?
|
||||||
|
@type.include?("game")
|
||||||
|
end
|
||||||
|
|
||||||
|
def servicable?
|
||||||
|
@servicable
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(context = nil)
|
||||||
|
{
|
||||||
|
"id": @id,
|
||||||
|
"category": @category,
|
||||||
|
"name": @name,
|
||||||
|
"type": @type,
|
||||||
|
"channels": @channels,
|
||||||
|
"extended-data": @extended_data,
|
||||||
|
"web-links": @web_links
|
||||||
|
}.to_json(context)
|
||||||
|
end
|
||||||
|
|
||||||
|
class Channel
|
||||||
|
attr_reader :id, :name, :user_level, :version, :extended_data
|
||||||
|
|
||||||
|
def initialize(data, app)
|
||||||
|
@_app = app
|
||||||
|
|
||||||
|
@id = data["id"]
|
||||||
|
@name = data["name"]
|
||||||
|
@user_level = data["user-level"]
|
||||||
|
@version = Gem::Version.new(data["current-version"])
|
||||||
|
@extended_data = data["extended-data"]&.map { |d| ExtendedData.new(d) } || nil
|
||||||
|
end
|
||||||
|
|
||||||
|
def extended_data(key, default)
|
||||||
|
v = @extended_data&.find { |d| d.name == key }&.value
|
||||||
|
if v.nil?
|
||||||
|
v = @_app.extended_data(key, default)
|
||||||
|
|
||||||
|
return default if v.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
v
|
||||||
|
end
|
||||||
|
|
||||||
|
def server_channel
|
||||||
|
extended_data("serverchannel", @id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(context = nil)
|
||||||
|
{
|
||||||
|
"id": @id,
|
||||||
|
"name": @name,
|
||||||
|
"user-level": @user_level,
|
||||||
|
"current-version": @version.to_s,
|
||||||
|
"extended-data": @extended_data
|
||||||
|
}.to_json(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class ExtendedData
|
||||||
|
attr_reader :name, :value
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
if data.is_a?(Hash)
|
||||||
|
@name = data["name"]
|
||||||
|
@value = data["value"]
|
||||||
|
else
|
||||||
|
@name = data[0]
|
||||||
|
@value = data[1]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(context = nil)
|
||||||
|
{
|
||||||
|
"name": @name,
|
||||||
|
"value": @value
|
||||||
|
}.to_json(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class WebLink
|
||||||
|
attr_reader :name, :uri
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
@name = data["name"]
|
||||||
|
@uri = data["uri"]
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(context = nil)
|
||||||
|
{
|
||||||
|
"name": @name,
|
||||||
|
"uri": @uri
|
||||||
|
}.to_json(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
129
lib/worker/api/game_server.rb
Normal file
129
lib/worker/api/game_server.rb
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
module W3DHubLauncher
|
||||||
|
class Worker
|
||||||
|
class Api
|
||||||
|
# Designed to work with cyberarm's Game Server Hub service data, may work with W3D Hub's service with degraded metadata.
|
||||||
|
class GameServer
|
||||||
|
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,
|
||||||
|
:match_start_time, :estimated_end_time, :match_remaining_time,
|
||||||
|
:teams, :next_map, :version
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
# main data
|
||||||
|
@id = data["id"]
|
||||||
|
@game = data["game"]
|
||||||
|
@channel = data["channel"]
|
||||||
|
@address = data["address"]
|
||||||
|
@port = data["port"]
|
||||||
|
@region = data["region"]
|
||||||
|
|
||||||
|
# status data
|
||||||
|
@name = data["status"]["name"]
|
||||||
|
@password = data["status"]["password"] || false
|
||||||
|
@current_map = data["status"]["map"]
|
||||||
|
@player_count = data["status"]["numplayers"] || 0
|
||||||
|
@max_players = data["status"]["maxplayers"]
|
||||||
|
|
||||||
|
@match_start_time = data["status"]["started"]
|
||||||
|
@estimated_end_time = data["status"]["estimatedEndTime"]
|
||||||
|
@match_remaining_time = data["status"]["remaining"]
|
||||||
|
|
||||||
|
# status teams data
|
||||||
|
@teams = []
|
||||||
|
data["status"]["teams"].each do |team_data|
|
||||||
|
@teams << Team.new(team_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# status player data
|
||||||
|
data["status"]["players"].each do |player_data|
|
||||||
|
team = @teams.find { |t| t.id == player_data["team"] }
|
||||||
|
next unless team
|
||||||
|
|
||||||
|
team.players << Player.new(player_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Cyberarm GSH extras
|
||||||
|
@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 :)
|
||||||
|
def time_elapsed
|
||||||
|
"00:00"
|
||||||
|
end
|
||||||
|
|
||||||
|
def password?
|
||||||
|
@password
|
||||||
|
end
|
||||||
|
|
||||||
|
def ping
|
||||||
|
MemCache.dig(:game_server_pings, @address) || BAD_OR_UNKNOWN_PING
|
||||||
|
end
|
||||||
|
|
||||||
|
def ping_score_ratio
|
||||||
|
# under 50 is best, over 200 is worst
|
||||||
|
case ping
|
||||||
|
when 0..49
|
||||||
|
return 1.0
|
||||||
|
when 50..200
|
||||||
|
1.0 - (ping / 200.0) + 0.1
|
||||||
|
else
|
||||||
|
return 0.1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def ping_score_color
|
||||||
|
case ping
|
||||||
|
when 0..49 # gentle green
|
||||||
|
0xff_26a269
|
||||||
|
when 50..149 # rich orange
|
||||||
|
0xff_e5a50a
|
||||||
|
when 150..200 # dark red
|
||||||
|
0xff_a51d2d
|
||||||
|
else # dark gray
|
||||||
|
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
|
||||||
|
attr_reader :id, :name, :score, :kills, :deaths, :players
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
@id = data["id"]
|
||||||
|
@name = data["name"]
|
||||||
|
@score = data["score"]
|
||||||
|
@kills = data["kills"]
|
||||||
|
@deaths = data["deaths"]
|
||||||
|
|
||||||
|
@players = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Player
|
||||||
|
attr_reader :name, :team, :score, :kills, :deaths, :ping, :time
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
@name = data["nick"]
|
||||||
|
@team = data["team"]
|
||||||
|
@score = data["score"]
|
||||||
|
@kills = data["kills"]
|
||||||
|
@deaths = data["deaths"]
|
||||||
|
|
||||||
|
# Cyberarm GSH extras
|
||||||
|
@ping = data["ping"] || 0
|
||||||
|
@time = data["time"] || "00:00:00"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
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
|
||||||
183
lib/worker/api/settings.rb
Normal file
183
lib/worker/api/settings.rb
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
module W3DHubLauncher
|
||||||
|
class Worker
|
||||||
|
class Api
|
||||||
|
|
||||||
|
# Inferred or automaticly set options
|
||||||
|
class Settings
|
||||||
|
SCHEMA = 0
|
||||||
|
|
||||||
|
attr_accessor :last_selected_app, :last_selected_app_channel, :last_selected_server_app, :preferences, :applications, :account
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
@schema = data["schema"] || SCHEMA
|
||||||
|
|
||||||
|
raise "Data is not a hash!" unless data.is_a?(Hash)
|
||||||
|
|
||||||
|
@last_selected_app = data["last_selected_app"] || "ren"
|
||||||
|
@last_selected_app_channel = data["last_selected_app_channel"] || "release"
|
||||||
|
@last_selected_server_app = data["last_selected_server_app"] || ""
|
||||||
|
|
||||||
|
@preferences = Preferences.new(data["preferences"])
|
||||||
|
@applications = (data["applications"] || []).map { |app_data| Application.new(app_data) }
|
||||||
|
@account = Account.new(data["account"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(options = {})
|
||||||
|
{
|
||||||
|
schema: @schema,
|
||||||
|
last_selected_app: @last_selected_app,
|
||||||
|
last_selected_app_channel: @last_selected_app_channel,
|
||||||
|
last_selected_server_app: @last_selected_server_app,
|
||||||
|
preferences: @preferences,
|
||||||
|
applications: @applications,
|
||||||
|
account: @account
|
||||||
|
}.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
|
||||||
|
|
||||||
|
attr_accessor :language, :nickname, :launcher_package_cache_directory, :application_installation_directory,
|
||||||
|
:wine_prefix_path, :wine_command, :winetricks_command
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
data ||= {}
|
||||||
|
|
||||||
|
@schema = data["schema"] || SCHEMA
|
||||||
|
|
||||||
|
@language = data["language"] || "en"
|
||||||
|
@nickname = data["nickname"] || ""
|
||||||
|
@launcher_package_cache_directory = data["launcher_package_cache_directory"] || DEFAULT_PACKAGE_CACHE_PATH
|
||||||
|
@application_installation_directory = data["application_installation_directory"] || DEFAULT_APPLICATIONS_PATH
|
||||||
|
@wine_prefix_path = data["wine_prefix_path"] || ""
|
||||||
|
@wine_command = data["wine_command"] || "wine"
|
||||||
|
@winetricks_command = data["winetricks_command"] || "winetricks"
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(options = {})
|
||||||
|
{
|
||||||
|
schema: @schema,
|
||||||
|
language: @language,
|
||||||
|
nickname: @nickname,
|
||||||
|
launcher_package_cache_directory: @launcher_package_cache_directory,
|
||||||
|
application_installation_directory: @application_installation_directory,
|
||||||
|
wine_prefix_path: @wine_prefix_path,
|
||||||
|
wine_command: @wine_command,
|
||||||
|
winetricks_command: @winetricks_command
|
||||||
|
}.to_json(options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Installed application data
|
||||||
|
class Application
|
||||||
|
SCHEMA = 0
|
||||||
|
|
||||||
|
attr_reader :id, :channel
|
||||||
|
attr_accessor :version, :installation_path, :wine_prefix_path, :launch_command, :timestamp
|
||||||
|
|
||||||
|
def self.create(id:, channel:, version:, installation_path:, wine_prefix_path:, launch_command:, timestamp:)
|
||||||
|
hash = {
|
||||||
|
"schema" => SCHEMA,
|
||||||
|
"id" => id,
|
||||||
|
"channel" => channel,
|
||||||
|
"version" => version,
|
||||||
|
"installation_path" => installation_path,
|
||||||
|
"wine_prefix_path" => wine_prefix_path,
|
||||||
|
"launch_command" => launch_command,
|
||||||
|
"timestamp" => timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
self.new(hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
@schema = data["schema"] || SCHEMA
|
||||||
|
|
||||||
|
@id = data["id"]
|
||||||
|
@channel = data["channel"]
|
||||||
|
@version = Gem::Version.new(data["version"])
|
||||||
|
@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 = {})
|
||||||
|
{
|
||||||
|
schema: @schema,
|
||||||
|
id: @id,
|
||||||
|
channel: @channel,
|
||||||
|
version: @version.to_s,
|
||||||
|
installation_path: @installation_path,
|
||||||
|
wine_prefix_path: @wine_prefix_path,
|
||||||
|
launch_command: @launch_command,
|
||||||
|
timestamp: @timestamp.to_i
|
||||||
|
}.to_json(options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# User account data
|
||||||
|
class Account
|
||||||
|
SCHEMA = 0
|
||||||
|
|
||||||
|
attr_accessor :user_id, :username, :displayname, :avatar_uri, :access_token, :refresh_token,
|
||||||
|
:access_token_expiration_time
|
||||||
|
|
||||||
|
def self.create(user_id:, username:, displayname:, avatar_uri:, access_token:, refresh_token:, access_token_expiration_time:)
|
||||||
|
hash = {
|
||||||
|
"schema" => SCHEMA,
|
||||||
|
"user_id" => user_id,
|
||||||
|
"username" => username,
|
||||||
|
"displayname" => displayname,
|
||||||
|
"avatar_uri" => avatar_uri,
|
||||||
|
"access_token" => access_token,
|
||||||
|
"refresh_token" => refresh_token,
|
||||||
|
"access_token_expiration_time" => access_token_expiration_time
|
||||||
|
}
|
||||||
|
|
||||||
|
self.new(hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(data)
|
||||||
|
data ||= {}
|
||||||
|
|
||||||
|
@schema = data["schema"] || SCHEMA
|
||||||
|
|
||||||
|
@user_id = data["user_id"] || -1
|
||||||
|
@username = data["username"] || ""
|
||||||
|
@displayname = data["displayname"] || ""
|
||||||
|
@avatar_uri = data["avatar_uri"] || ""
|
||||||
|
@access_token = data["access_token"] || ""
|
||||||
|
@refresh_token = data["refresh_token"] || ""
|
||||||
|
@access_token_expiration_time = Time.at(data["access_token_expiration_time"] || 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def signed_in?
|
||||||
|
@user_id.positive?
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(options = {})
|
||||||
|
{
|
||||||
|
schema: @schema,
|
||||||
|
user_id: @user_id,
|
||||||
|
username: @username,
|
||||||
|
displayname: @displayname,
|
||||||
|
avatar_uri: @avatar_uri,
|
||||||
|
access_token: @access_token,
|
||||||
|
refresh_token: @refresh_token,
|
||||||
|
access_token_expiration_time: @access_token_expiration_time.to_i
|
||||||
|
}.to_json(options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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,10 +3,6 @@ module W3DHubLauncher
|
|||||||
class Request
|
class Request
|
||||||
Query = Data.define(:type, :request_id, :data)
|
Query = Data.define(:type, :request_id, :data)
|
||||||
|
|
||||||
FETCH_URL = 0
|
|
||||||
DOWNLOAD_URL = 1
|
|
||||||
W3DHUB_API_CALL = 10
|
|
||||||
|
|
||||||
STATUS_ERROR = -1 # request has failed
|
STATUS_ERROR = -1 # request has failed
|
||||||
STATUS_PENDING = 0 # request has not yet started
|
STATUS_PENDING = 0 # request has not yet started
|
||||||
STATUS_OK = 1 # request completed successfully
|
STATUS_OK = 1 # request completed successfully
|
||||||
@@ -45,17 +41,17 @@ module W3DHubLauncher
|
|||||||
|
|
||||||
def enqueue(type, id, data)
|
def enqueue(type, id, data)
|
||||||
Request.requests << self
|
Request.requests << self
|
||||||
W3DHubLauncher::WORKER.send(Query.new(type, id, data))
|
W3DHubLauncher::WORKER.request(Query.new(type, id, data))
|
||||||
end
|
end
|
||||||
|
|
||||||
# event from Worker received
|
# event from Worker received
|
||||||
def handle_event(event, data)
|
def handle_event(event, data)
|
||||||
pp [event, data]
|
# pp [event, data]
|
||||||
|
|
||||||
|
@callback&.call(data, event)
|
||||||
|
|
||||||
case event
|
case event
|
||||||
when STATUS_COMPLETE
|
when STATUS_ERROR, STATUS_COMPLETE
|
||||||
Request.requests.delete(self)
|
|
||||||
when STATUS_ERROR
|
|
||||||
Request.requests.delete(self)
|
Request.requests.delete(self)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,561 @@
|
|||||||
module W3DHubLauncher
|
module W3DHubLauncher
|
||||||
class Task
|
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
|
||||||
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,11 +6,29 @@ module W3DHubLauncher
|
|||||||
PRIMARY_W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze
|
PRIMARY_W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze
|
||||||
ALTERNATIVE_W3DHUB_API_ENDPOINT = "https://backend.w3d.cyberarm.dev".freeze
|
ALTERNATIVE_W3DHUB_API_ENDPOINT = "https://backend.w3d.cyberarm.dev".freeze
|
||||||
|
|
||||||
|
attr_reader :applications
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@access_token = nil
|
@access_token = nil
|
||||||
|
|
||||||
|
@applications = []
|
||||||
|
|
||||||
|
@http_clients = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
def headers(form_encoded: false)
|
def headers(form_encoded: false, range: nil)
|
||||||
|
array = [
|
||||||
|
["user-agent", W3DHubLauncher::USER_AGENT],
|
||||||
|
["accept", "application/json"],
|
||||||
|
]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
array
|
||||||
end
|
end
|
||||||
|
|
||||||
# return raw response to requester
|
# return raw response to requester
|
||||||
@@ -20,13 +38,20 @@ module W3DHubLauncher
|
|||||||
Sync do |task|
|
Sync do |task|
|
||||||
task.with_timeout(API_TIMEOUT) do
|
task.with_timeout(API_TIMEOUT) do
|
||||||
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
||||||
|
pp [method, url, headers, body, response]
|
||||||
|
if response.success?
|
||||||
result.data = response.read
|
result.data = response.read
|
||||||
|
else
|
||||||
|
result.error = true
|
||||||
|
end
|
||||||
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result.error = e
|
result.error = e
|
||||||
end
|
rescue Async::TimeoutError => e
|
||||||
rescue Async::TimeoutError
|
|
||||||
result.error = e
|
result.error = e
|
||||||
end
|
end
|
||||||
|
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -37,13 +62,15 @@ module W3DHubLauncher
|
|||||||
result = CyberarmEngine::Result.new
|
result = CyberarmEngine::Result.new
|
||||||
|
|
||||||
Sync do |task|
|
Sync do |task|
|
||||||
task.with_timeout(API_TIMEOUT) do
|
|
||||||
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
Async::HTTP::Internet.send(method, url, headers, body) do |response|
|
||||||
if response.success?
|
if response.success?
|
||||||
content_length = response.headers["content-length"] || 0
|
content_length = response.headers["content-length"] || 0
|
||||||
|
|
||||||
total_downloaded_bytes = 0
|
total_downloaded_bytes = 0
|
||||||
File.open(path, "wb") do |file|
|
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?
|
||||||
|
|
||||||
response.each do |chunk|
|
response.each do |chunk|
|
||||||
file.write(chunk)
|
file.write(chunk)
|
||||||
downloaded_bytes = chunk.length
|
downloaded_bytes = chunk.length
|
||||||
@@ -57,7 +84,6 @@ module W3DHubLauncher
|
|||||||
end
|
end
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
result.error = e
|
result.error = e
|
||||||
end
|
|
||||||
rescue Async::TimeoutError
|
rescue Async::TimeoutError
|
||||||
result.error = e
|
result.error = e
|
||||||
end
|
end
|
||||||
@@ -80,29 +106,60 @@ module W3DHubLauncher
|
|||||||
|
|
||||||
def fetch_applications
|
def fetch_applications
|
||||||
result = CyberarmEngine::Result.new
|
result = CyberarmEngine::Result.new
|
||||||
|
# If we're not signed in, then the primary backend will try to redirect us to the alternate backend. So skip it if we're not signed in.
|
||||||
|
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
|
||||||
|
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
|
||||||
|
single_source = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_news()
|
unless single_source
|
||||||
result = CyberarmEngine::Result.new
|
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
|
end
|
||||||
|
|
||||||
def fetch_events()
|
@applications = JSON.parse(result.data)["applications"]&.map { |app| W3DHubLauncher::Worker::Api::Application.new(app) } || []
|
||||||
result = CyberarmEngine::Result.new
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
|
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(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
|
end
|
||||||
|
|
||||||
def fetch_manifest()
|
def fetch_manifest()
|
||||||
result = CyberarmEngine::Result.new
|
result = CyberarmEngine::Result.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_manifests()
|
def fetch_package_details(packages)
|
||||||
result = CyberarmEngine::Result.new
|
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
|
end
|
||||||
|
|
||||||
def fetch_package_details()
|
def download_package()
|
||||||
result = CyberarmEngine::Result.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_package()
|
|
||||||
download()
|
download()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
287
lib/ww_mix.rb
Normal file
287
lib/ww_mix.rb
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
require "digest"
|
||||||
|
require "stringio"
|
||||||
|
|
||||||
|
module W3DHubLauncher
|
||||||
|
# Reimplementating MIX1 reader/writer with years more
|
||||||
|
# experience working with these formats and having the
|
||||||
|
# advantage of being able to reference the renegade source
|
||||||
|
# code :)
|
||||||
|
class WWMix
|
||||||
|
MIX1_HEADER = 0x3158494D
|
||||||
|
MIX2_HEADER = 0x3258494D
|
||||||
|
|
||||||
|
MixHeader = Struct.new(
|
||||||
|
:mime_type, # int32
|
||||||
|
:file_data_offset, # int32
|
||||||
|
:file_names_offset, # int32
|
||||||
|
:_reserved # int32
|
||||||
|
)
|
||||||
|
|
||||||
|
EntryInfoHeader = Struct.new(
|
||||||
|
:crc32, # uint32
|
||||||
|
:content_offset, # uint32
|
||||||
|
:content_length # uint32
|
||||||
|
)
|
||||||
|
|
||||||
|
class Entry
|
||||||
|
attr_accessor :path, :name, :info, :blob, :is_blob
|
||||||
|
|
||||||
|
def initialize(name:, path:, info:, blob: nil)
|
||||||
|
@name = name
|
||||||
|
@path = path
|
||||||
|
@info = info
|
||||||
|
@blob = blob
|
||||||
|
|
||||||
|
@info.content_length = blob.size if blob?
|
||||||
|
end
|
||||||
|
|
||||||
|
def blob?
|
||||||
|
@blob
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_crc32
|
||||||
|
Digest::CRC32.hexdigest(@name.upcase).upcase.to_i(16)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Write entry's data to stream.
|
||||||
|
# Caller is responsible for ensuring stream is valid for writing
|
||||||
|
def copy_to(stream)
|
||||||
|
if blob?
|
||||||
|
return false if @blob.size.zero?
|
||||||
|
|
||||||
|
stream.write(blob)
|
||||||
|
return true
|
||||||
|
else
|
||||||
|
if read
|
||||||
|
stream.write(@blob)
|
||||||
|
@blob = nil
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def read
|
||||||
|
return false unless File.exist?(@path)
|
||||||
|
return false if File.directory?(@path)
|
||||||
|
return false if File.size(@path) < @info.content_offset + @info.content_length
|
||||||
|
|
||||||
|
@blob = File.binread(@path, @info.content_length, @info.content_offset)
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :path, :encrypted, :entries, :error_reason
|
||||||
|
|
||||||
|
def initialize(path:, encrypted: false)
|
||||||
|
@path = path
|
||||||
|
@encrypted = encrypted
|
||||||
|
@entries = []
|
||||||
|
|
||||||
|
@error_reason = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load entries from MIX file. Entry data is NOT loaded.
|
||||||
|
# @return true on success or false on failure. Check m_error_reason for why.
|
||||||
|
def load
|
||||||
|
unless File.exist?(@path)
|
||||||
|
@error_reason = format("Path does not exist: %s", @path)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if File.directory?(@path)
|
||||||
|
@error_reason = format("Path is a directory: %s", @path)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
File.open(@path, "rb") do |f|
|
||||||
|
header = MixHeader.new(0, 0, 0, 0)
|
||||||
|
header.mime_type = read_i32(f)
|
||||||
|
header.file_data_offset = read_i32(f)
|
||||||
|
header.file_names_offset = read_i32(f)
|
||||||
|
header._reserved = read_i32(f)
|
||||||
|
|
||||||
|
unless header.mime_type == MIX1_HEADER || header.mime_type == MIX2_HEADER
|
||||||
|
@error_reason = format("Invalid mime type: %d", header.mime_type)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
@encrypted = header.mime_type == MIX2_HEADER
|
||||||
|
|
||||||
|
# Read entry info
|
||||||
|
f.pos = header.file_data_offset
|
||||||
|
file_count = read_i32(f)
|
||||||
|
|
||||||
|
file_count.times do |i|
|
||||||
|
entry_info = EntryInfoHeader.new(0, 0, 0)
|
||||||
|
entry_info.crc32 = read_u32(f)
|
||||||
|
entry_info.content_offset = read_u32(f)
|
||||||
|
entry_info.content_length = read_u32(f)
|
||||||
|
|
||||||
|
@entries << Entry.new(name: "", path: @path, info: entry_info)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Read entry names
|
||||||
|
f.pos = header.file_names_offset
|
||||||
|
file_count = read_i32(f)
|
||||||
|
|
||||||
|
file_count.times do |i|
|
||||||
|
@entries[i].name = read_string(f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def save
|
||||||
|
unless @entries.size.positive?
|
||||||
|
@error_reason = "No entries to write."
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
if File.directory?(@path)
|
||||||
|
@error_reason = format("Path is a directory: %s", @path)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
File.open(@path, "wb") do |f|
|
||||||
|
header = MixHeader.new(encrypted? ? MIX2_HEADER : MIX1_HEADER, 0, 0, 0)
|
||||||
|
|
||||||
|
# write mime type
|
||||||
|
write_i32(f, header.mime_type)
|
||||||
|
|
||||||
|
f.pos = 16
|
||||||
|
|
||||||
|
# sort entries by crc32 of their name
|
||||||
|
sort_entries
|
||||||
|
|
||||||
|
# write file blobs
|
||||||
|
@entries.each do |entry|
|
||||||
|
# store current io position
|
||||||
|
pos = f.pos
|
||||||
|
|
||||||
|
# copy entry to stream
|
||||||
|
entry.copy_to(f)
|
||||||
|
|
||||||
|
# update entry with new offset
|
||||||
|
entry.info.content_offset = pos
|
||||||
|
|
||||||
|
# add alignment padding
|
||||||
|
padding = (-f.pos & 7)
|
||||||
|
padding.times do |i|
|
||||||
|
write_u8(f, 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Save file data offset
|
||||||
|
header.file_data_offset = f.pos
|
||||||
|
|
||||||
|
# write number of entries
|
||||||
|
write_i32(f, @entries.size)
|
||||||
|
|
||||||
|
# write entries file data
|
||||||
|
@entries.each do |entry|
|
||||||
|
write_u32(f, entry.info.crc32)
|
||||||
|
write_u32(f, entry.info.content_offset)
|
||||||
|
write_u32(f, entry.info.content_length)
|
||||||
|
end
|
||||||
|
|
||||||
|
# save file names offset
|
||||||
|
header.file_names_offset = f.pos
|
||||||
|
|
||||||
|
# write number of entries
|
||||||
|
write_i32(f, @entries.size)
|
||||||
|
|
||||||
|
# write entry names
|
||||||
|
@entries.each do |entry|
|
||||||
|
write_string(f, entry.name)
|
||||||
|
end
|
||||||
|
|
||||||
|
# jump to io_position 4
|
||||||
|
f.pos = 4
|
||||||
|
# write rest of header
|
||||||
|
|
||||||
|
write_i32(f, header.file_data_offset)
|
||||||
|
write_i32(f, header.file_names_offset)
|
||||||
|
write_i32(f, header._reserved)
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
# ALL entries MUST have unique case-insensitive names
|
||||||
|
@entries.each do |a|
|
||||||
|
@entries.each do |b|
|
||||||
|
next if a == b
|
||||||
|
|
||||||
|
return false if a.name.upcase == b.name.upcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypted?
|
||||||
|
@encrypted
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_file(path:, replace: false)
|
||||||
|
return false unless File.exist?(path)
|
||||||
|
return false if File.directory?(path)
|
||||||
|
|
||||||
|
entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
|
||||||
|
add_entry(entry: entry, replace: replace)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_blob(path:, blob:, replace: false)
|
||||||
|
info = EntryInfoHeader.new(0, 0, blob.size)
|
||||||
|
entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
||||||
|
into.crc32 = @entries.last.calculate_crc32
|
||||||
|
|
||||||
|
add_entry(entry: entry, replace: replace)
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_entry(entry:, replace: false)
|
||||||
|
duplicate = @entries.find { |e| e.name.upcase == entry.name.upcase }
|
||||||
|
|
||||||
|
if duplicate
|
||||||
|
if replace
|
||||||
|
@entries.delete(duplicate)
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@entries << entry
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def sort_entries
|
||||||
|
return false if @entries.any? { |e| e.info.crc32 == 0 }
|
||||||
|
|
||||||
|
@entries.sort! { |a, b| a.info.crc32 <=> b.info.crc32 }
|
||||||
|
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_i32(f) = f.read(4).unpack1("l")
|
||||||
|
def read_u32(f) = f.read(4).unpack1("L")
|
||||||
|
def read_u8(f) = f.read(1).unpack1("C")
|
||||||
|
def read_string(f)
|
||||||
|
f.read(read_u8(f)).strip
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_i32(f, value) = f.write([value].pack("l"))
|
||||||
|
def write_u32(f, value) = f.write([value].pack("L"))
|
||||||
|
def write_u8(f, value) = f.write([value].pack("C"))
|
||||||
|
def write_string(f, string)
|
||||||
|
length = string.size + 1 # include null byte
|
||||||
|
write_u8(f, length)
|
||||||
|
f.write(string)
|
||||||
|
write_u8(f, 0) # null byte
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
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: 24 KiB |
@@ -4,9 +4,14 @@ rescue LoadError
|
|||||||
require "cyberarm_engine"
|
require "cyberarm_engine"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
require "socket"
|
||||||
require "rexml"
|
require "rexml"
|
||||||
require "base64"
|
require "base64"
|
||||||
require "logger"
|
require "logger"
|
||||||
|
require "digest"
|
||||||
|
require "pathname"
|
||||||
|
require "fileutils"
|
||||||
|
require "tempfile"
|
||||||
|
|
||||||
require "async"
|
require "async"
|
||||||
require "async/http/internet/instance"
|
require "async/http/internet/instance"
|
||||||
@@ -14,6 +19,10 @@ require "async/websocket"
|
|||||||
require "digest/crc"
|
require "digest/crc"
|
||||||
require "ircparser"
|
require "ircparser"
|
||||||
require "zip"
|
require "zip"
|
||||||
|
require "chunky_png"
|
||||||
|
require "sdl3"
|
||||||
|
|
||||||
|
require_relative "lib/gosu_ext"
|
||||||
|
|
||||||
require_relative "lib/version"
|
require_relative "lib/version"
|
||||||
require_relative "lib/constants"
|
require_relative "lib/constants"
|
||||||
@@ -28,12 +37,29 @@ require_relative "lib/pages/boot/terms"
|
|||||||
require_relative "lib/pages/boot/initial_setup"
|
require_relative "lib/pages/boot/initial_setup"
|
||||||
require_relative "lib/pages/boot/start_up"
|
require_relative "lib/pages/boot/start_up"
|
||||||
require_relative "lib/dialogs/about"
|
require_relative "lib/dialogs/about"
|
||||||
|
require_relative "lib/dialogs/downloads"
|
||||||
|
require_relative "lib/dialogs/server"
|
||||||
|
require_relative "lib/dialogs/account"
|
||||||
|
require_relative "lib/dialogs/import_application"
|
||||||
require_relative "lib/states/boot"
|
require_relative "lib/states/boot"
|
||||||
require_relative "lib/states/interface"
|
require_relative "lib/states/interface"
|
||||||
require_relative "lib/window"
|
require_relative "lib/window"
|
||||||
|
require_relative "lib/application_helper"
|
||||||
|
|
||||||
|
require_relative "lib/ico"
|
||||||
|
require_relative "lib/dds"
|
||||||
|
require_relative "lib/ww_mix"
|
||||||
|
|
||||||
require_relative "lib/worker"
|
require_relative "lib/worker"
|
||||||
require_relative "lib/worker/api"
|
require_relative "lib/worker/api"
|
||||||
|
require_relative "lib/worker/api/application"
|
||||||
|
require_relative "lib/worker/api/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/request"
|
||||||
require_relative "lib/worker/w3dhub_api"
|
require_relative "lib/worker/w3dhub_api"
|
||||||
require_relative "lib/worker/task"
|
require_relative "lib/worker/task"
|
||||||
@@ -41,34 +67,28 @@ require_relative "lib/worker/tasks/install_application"
|
|||||||
require_relative "lib/worker/tasks/uninstall_application"
|
require_relative "lib/worker/tasks/uninstall_application"
|
||||||
require_relative "lib/worker/tasks/repair_application"
|
require_relative "lib/worker/tasks/repair_application"
|
||||||
require_relative "lib/worker/tasks/update_application"
|
require_relative "lib/worker/tasks/update_application"
|
||||||
|
require_relative "lib/worker/tasks/move_application"
|
||||||
|
|
||||||
module W3DHubLauncher
|
module W3DHubLauncher
|
||||||
WORKER = Ractor.new(name: "Parallel Worker") { W3DHubLauncher::Worker.new }
|
MemCache = {}
|
||||||
end
|
|
||||||
|
|
||||||
# Hello, I exist because there presently exists no way to ask if there are pending
|
# UNIXServer
|
||||||
# messages in our ractors mailbox without making a blocking call which is a big no no
|
|
||||||
# for a GUI application. :|
|
|
||||||
#
|
|
||||||
# Keep an eye on: https://bugs.ruby-lang.org/issues/21930: "Add Ractor#empty? method to check for pending messages without blocking"
|
|
||||||
#
|
|
||||||
# NOTE: May need to mangle Window#update to do ruby-land sleep so thread gets time to process :(
|
|
||||||
Thread.new do
|
Thread.new do
|
||||||
loop do
|
W3DHubLauncher::Worker.new.listen
|
||||||
response = Ractor.receive
|
|
||||||
pp response
|
|
||||||
|
|
||||||
request = W3DHubLauncher::Worker::Request.requests.find { |r| r.request_id == response.request_id }
|
|
||||||
request&.handle_event(response.status, response.data)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
10.times do
|
# UNIXSocket / client
|
||||||
W3DHubLauncher::Worker::Request.new(W3DHubLauncher::Worker::Request::W3DHUB_API_CALL, { call: :fetch_applications }) do |result|
|
WORKER = W3DHubLauncher::Worker.new
|
||||||
pp result
|
WORKER.connect
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SDL.load_lib("SDL3")
|
||||||
|
|
||||||
window = W3DHubLauncher::Window.new(width: 1280, height: 800, resizable: true)
|
window = W3DHubLauncher::Window.new(width: 1280, height: 800, resizable: true)
|
||||||
# window = W3DHubLauncher::Window.new(width: 1920, height: 1080, resizable: true)
|
# window = W3DHubLauncher::Window.new(width: 1920, height: 1080, resizable: true)
|
||||||
|
|
||||||
|
# Gosu.choose_file do |files|
|
||||||
|
# pp files
|
||||||
|
# end
|
||||||
|
|
||||||
window.show
|
window.show
|
||||||
|
|||||||
Reference in New Issue
Block a user