mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-03-22 04:06:18 +00:00
Compare commits
13 Commits
developmen
...
46dece0479
| Author | SHA1 | Date | |
|---|---|---|---|
| 46dece0479 | |||
| 68af00bd2f | |||
| 208f2b8a39 | |||
| b5d975761c | |||
| 44483117d8 | |||
| 4146debc4c | |||
| 1f4185ada2 | |||
| e2496d0a09 | |||
| d4e4697983 | |||
| 3e949cf863 | |||
| 027ba27b37 | |||
| adff1f9159 | |||
| 355a4503ea |
27
Gemfile
27
Gemfile
@@ -1,26 +1,15 @@
|
|||||||
source "https://rubygems.org"
|
source "https://rubygems.org"
|
||||||
|
|
||||||
# "standard lib" gems
|
|
||||||
gem "base64"
|
gem "base64"
|
||||||
gem "rexml"
|
|
||||||
gem "logger"
|
|
||||||
|
|
||||||
# networking libs
|
|
||||||
gem "async-http"
|
gem "async-http"
|
||||||
gem "async-websocket"
|
gem "async-websocket"
|
||||||
|
|
||||||
# "game" library gem
|
|
||||||
gem "cyberarm_engine"
|
gem "cyberarm_engine"
|
||||||
gem "sdl2-bindings"
|
gem "sdl2-bindings"
|
||||||
|
gem "libui", platforms: [:windows]
|
||||||
# misc. libs
|
|
||||||
gem "digest-crc"
|
gem "digest-crc"
|
||||||
gem "ircparser"
|
gem "ircparser"
|
||||||
|
gem "rexml"
|
||||||
gem "rubyzip"
|
gem "rubyzip"
|
||||||
|
|
||||||
# file selection dialogs on windows (SDL3 has these built-in, but we're on SDL2)
|
|
||||||
gem "libui", platforms: [:windows]
|
|
||||||
# misc. windows only gems
|
|
||||||
gem "win32-process", platforms: [:windows]
|
gem "win32-process", platforms: [:windows]
|
||||||
gem "win32-security", platforms: [:windows]
|
gem "win32-security", platforms: [:windows]
|
||||||
|
|
||||||
@@ -29,9 +18,9 @@ gem "win32-security", platforms: [:windows]
|
|||||||
# use `bundle _x.y.z_ COMMAND` to use this one...
|
# use `bundle _x.y.z_ COMMAND` to use this one...
|
||||||
# NOTE: Releasy needs to be installed as a system gem i.e. `rake install`
|
# NOTE: Releasy needs to be installed as a system gem i.e. `rake install`
|
||||||
# NOTE: contents of the `gemhome` folder in the packaged folder need to be moved into the lib/ruby/gems\<RUBY_VERSION> folder
|
# NOTE: contents of the `gemhome` folder in the packaged folder need to be moved into the lib/ruby/gems\<RUBY_VERSION> folder
|
||||||
# group :windows_packaging do
|
# group :windows_packaging do
|
||||||
# gem "bundler", "~>2.4.3"
|
# gem "bundler", "~>2.4.3"
|
||||||
# gem "rake"
|
# gem "rake"
|
||||||
# gem "ocran"
|
# gem "ocran"
|
||||||
# gem "releasy"#, path: "../releasy"
|
# gem "releasy"#, path: "../releasy"
|
||||||
# end
|
# end
|
||||||
|
|||||||
22
Gemfile.lock
22
Gemfile.lock
@@ -1,7 +1,7 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
async (2.38.1)
|
async (2.36.0)
|
||||||
console (~> 1.29)
|
console (~> 1.29)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
io-event (~> 1.11)
|
io-event (~> 1.11)
|
||||||
@@ -18,7 +18,7 @@ GEM
|
|||||||
protocol-http2 (~> 0.22)
|
protocol-http2 (~> 0.22)
|
||||||
protocol-url (~> 0.2)
|
protocol-url (~> 0.2)
|
||||||
traces (~> 0.10)
|
traces (~> 0.10)
|
||||||
async-pool (0.11.2)
|
async-pool (0.11.1)
|
||||||
async (>= 2.0)
|
async (>= 2.0)
|
||||||
async-websocket (0.30.0)
|
async-websocket (0.30.0)
|
||||||
async-http (~> 0.76)
|
async-http (~> 0.76)
|
||||||
@@ -26,7 +26,7 @@ GEM
|
|||||||
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)
|
console (1.34.2)
|
||||||
fiber-annotation
|
fiber-annotation
|
||||||
fiber-local (~> 1.1)
|
fiber-local (~> 1.1)
|
||||||
json
|
json
|
||||||
@@ -43,30 +43,29 @@ GEM
|
|||||||
fiber-storage (1.0.1)
|
fiber-storage (1.0.1)
|
||||||
fiddle (1.1.8)
|
fiddle (1.1.8)
|
||||||
gosu (1.4.6)
|
gosu (1.4.6)
|
||||||
io-endpoint (0.17.2)
|
io-endpoint (0.17.1)
|
||||||
io-event (1.14.4)
|
io-event (1.14.2)
|
||||||
io-stream (0.11.1)
|
io-stream (0.11.1)
|
||||||
ircparser (1.0.0)
|
ircparser (1.0.0)
|
||||||
json (2.19.2)
|
json (2.18.0)
|
||||||
libui (0.2.0-x64-mingw-ucrt)
|
libui (0.2.0-x64-mingw-ucrt)
|
||||||
fiddle
|
fiddle
|
||||||
logger (1.7.0)
|
|
||||||
metrics (0.15.0)
|
metrics (0.15.0)
|
||||||
protocol-hpack (1.5.1)
|
protocol-hpack (1.5.1)
|
||||||
protocol-http (0.60.0)
|
protocol-http (0.58.1)
|
||||||
protocol-http1 (0.37.0)
|
protocol-http1 (0.37.0)
|
||||||
protocol-http (~> 0.58)
|
protocol-http (~> 0.58)
|
||||||
protocol-http2 (0.24.0)
|
protocol-http2 (0.24.0)
|
||||||
protocol-hpack (~> 1.4)
|
protocol-hpack (~> 1.4)
|
||||||
protocol-http (~> 0.47)
|
protocol-http (~> 0.47)
|
||||||
protocol-rack (0.22.0)
|
protocol-rack (0.21.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.4.0)
|
||||||
protocol-websocket (0.20.2)
|
protocol-websocket (0.20.2)
|
||||||
protocol-http (~> 0.2)
|
protocol-http (~> 0.2)
|
||||||
rack (3.2.5)
|
rack (3.2.4)
|
||||||
rake (13.3.1)
|
rake (13.3.1)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rubyzip (3.2.2)
|
rubyzip (3.2.2)
|
||||||
@@ -91,7 +90,6 @@ DEPENDENCIES
|
|||||||
digest-crc
|
digest-crc
|
||||||
ircparser
|
ircparser
|
||||||
libui
|
libui
|
||||||
logger
|
|
||||||
rexml
|
rexml
|
||||||
rubyzip
|
rubyzip
|
||||||
sdl2-bindings
|
sdl2-bindings
|
||||||
@@ -99,4 +97,4 @@ DEPENDENCIES
|
|||||||
win32-security
|
win32-security
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
4.0.3
|
2.6.8
|
||||||
|
|||||||
35
docs/README.md
Normal file
35
docs/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Documentation
|
||||||
|
|
||||||
|
## File Formats
|
||||||
|
|
||||||
|
Documentation and examples of file formats.
|
||||||
|
|
||||||
|
### [Legacy Manifest](file_formats/LEGACY_MANIFEST.md)
|
||||||
|
|
||||||
|
Blue Hell Productions `manifest.xml`. Still in use by W3D Hub.
|
||||||
|
|
||||||
|
### [Megafest](file_formats/MEGAFEST.md)
|
||||||
|
|
||||||
|
Prototype new thing `megafest.json`
|
||||||
|
|
||||||
|
### [MIX](file_formats/MIX.md)
|
||||||
|
|
||||||
|
Westwood archive format `<name>.mix`, `<name>.dat` and `<name>.pkg`
|
||||||
|
|
||||||
|
### [W3D Hub Patch](file_formats/W3D_HUB_PATCH.md)
|
||||||
|
|
||||||
|
Describes how to update a MIX archive from a package patch
|
||||||
|
|
||||||
|
## Protocols
|
||||||
|
|
||||||
|
### [LAN Discovery](protocols/LAN_DISCOVERY.md)
|
||||||
|
|
||||||
|
How launchers and other applications discovery each other.
|
||||||
|
|
||||||
|
### [Launcher Remote](protocols/LAUNCHER_REMOTE.md)
|
||||||
|
|
||||||
|
Remotely control and get progress reports from the launcher
|
||||||
|
|
||||||
|
### [LAN Package Share](protocols/LAN_PACKAGE_SHARE.md)
|
||||||
|
|
||||||
|
Enable launchers on the same network to discover each other and share packages over LAN.
|
||||||
73
docs/file_formats/LEGACY_MANIFEST.md
Normal file
73
docs/file_formats/LEGACY_MANIFEST.md
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# Legacy Manifest
|
||||||
|
|
||||||
|
In use since Blue Hell Productions
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> References to packages exclude `.zip`
|
||||||
|
|
||||||
|
Example `Full` manifest:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
|
||||||
|
<BHP_Game_Manifest game="apb" version="0.9935.1.0" type="Full">
|
||||||
|
<Dependency name="msvc-2015.zip"/>
|
||||||
|
<Dependency name="directx-43.zip"/>
|
||||||
|
<File name="game.exe" checksum="C7AE972A2B9CF6EFCADE7919323F048F5CFAF2D40BD5A67D5A91F8D8A3759CA4" package="binaries"/>
|
||||||
|
<File name="MemoryManager.dll" checksum="B60A2B3F6A07D729EE5C2902CB9A6BEDAF55EEDB13F8514BAC0AD6F5D51DD0C2" package="binaries"/>
|
||||||
|
<File name="scripts.dll" checksum="76CAAEBF21E4D6A34C3ABDD474D59580DD1EAB097C9585A1FFC0E28FF0FCAF56" package="binaries"/>
|
||||||
|
<File name="Scripts2.dll" checksum="A0439B3BFE6CB497F402291548A312A9F43C0D38BDAF26C83E21DBD3F4D55702" package="binaries"/>
|
||||||
|
<File name="shared.dll" checksum="17A8797EC5F4C34E241681F4AF76EEA93856D6A1854EC9838B263AEC89CDF036" package="binaries"/>
|
||||||
|
<File name="ttle.dll" checksum="C005EB50D77675CE712B30A4402AB0D152D3993EB509E35AEDCB834CED54FADF" package="binaries"/>
|
||||||
|
<File name="ttversion.txt" checksum="17A9E627A03E20AC33849330468BF8A80B48449C9D473DF45F1052A160083167" package="misc"/>
|
||||||
|
<File name="Data/always.dat" checksum="D4D09149C7B5368AB3947AEC7B6E7781208F592FCA4390261F04A8E9C1887EB3" package="always"/>
|
||||||
|
<File name="Data/RA_Volcano.mix" checksum="4F3CB80BCF200659B3B4B167CCB64ECB36A3BA917681F4AB8027568EC187B88D" package="RA_Volcano"/>
|
||||||
|
<File name="Data/Movies/ea_ww.bik" checksum="065B8B0894CF9C1C09A940D26069C54A771EBB3092143BA37E97F8CA85B8CCAD" package="movies"/>
|
||||||
|
<File name="Data/Movies/R_Intro.BIK" checksum="0D9A2B2ABFD9680DF1C8D0F876DA4B60F015CA5C56EC6C0593528A3F88E2A58F" package="movies"/>
|
||||||
|
</BHP_Game_Manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example `Patch` manifest:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
|
||||||
|
<BHP_Game_Manifest game="apb" version="3.6.4.1" type="Patch" baseVersion="3.6.4.0">
|
||||||
|
<Dependency name="msvc-2022.zip"/>
|
||||||
|
<Dependency name="msvc-2015.zip" removedsince="3.6.4.1"/>
|
||||||
|
<File name="data/Always.dat" checksum="7B27D547E5A50401C9CA2851ACD4AC3F3CDA104F34650E1C32EB63A4120E3C90">
|
||||||
|
<Patch from="3.6.4.0" package="Always.patch.3.6.4.0"/>
|
||||||
|
</File>
|
||||||
|
<File name="data/Always_Emitters.dat" checksum="5C3EA8E5C4D7278F5EF1AF3CE1FC4F698C5A7D1FE7116DA0376A709E7CD3B2AE">
|
||||||
|
<Patch from="3.6.3.0" package="Always_Emitters.patch.3.6.3.0"/>
|
||||||
|
</File>
|
||||||
|
<File name="data/Always_Vehicles.dat" checksum="C6D4F6B3412C26065DE9B9F739BEC041B49FD613DB2686D8F231C2F692B263C5">
|
||||||
|
<Patch from="3.6.4.0" package="Always_Vehicles.patch.3.6.4.0"/>
|
||||||
|
</File>
|
||||||
|
<File name="data/RA_TestUnits.mix" removedsince="3.6.4.1"/>
|
||||||
|
<File name="data/RA_RidgeWar.mix" checksum="C3438C532C27F535CD958BEA9702160E952BC6CE58F9B43778F91DA23C6AE9FE" package="RA_RidgeWar"/>
|
||||||
|
<File name="data/RA_SwampOfIllusions.mix" checksum="A0787979664E5977A1949587382A3A84B9F1526F81934EBB7A3AC9334F9D4059">
|
||||||
|
<Patch from="3.6.4.0" package="RA_SwampOfIllusions.patch.3.6.4.0"/>
|
||||||
|
</File>
|
||||||
|
</BHP_Game_Manifest>
|
||||||
|
```
|
||||||
|
|
||||||
|
## SPECIFICATION
|
||||||
|
### BHP_GAME_MANIFEST (Root Element)
|
||||||
|
* **GAME** - Unique Application ID
|
||||||
|
* **VERSION** - Application Unique Version for Build
|
||||||
|
* **TYPE** - Type of Build
|
||||||
|
* **Full** - Full Build
|
||||||
|
* **Patch** - Patch Build
|
||||||
|
* **BASEVERSION** - *OPTIONAL* Version this build was based upon
|
||||||
|
|
||||||
|
### DEPENDENCY
|
||||||
|
* **NAME** - Package name of dependency sans file extension
|
||||||
|
* **REMOVEDSINCE** - _OPTIONAL_ No data. Not used anymore?
|
||||||
|
|
||||||
|
### FILE
|
||||||
|
* **NAME** - Name of file
|
||||||
|
* **CHECKSUM** - _OPTIONAL_ SHA256 hash of file
|
||||||
|
* **PACKAGE** - _OPTIONAL_ Package that contains this file. Not present on removed files or files that have been patched.
|
||||||
|
* **REMOVEDSINCE** - _OPTIONAL_ Version this package was removed in. If present, only _NAME_ and _REMOVEDSINCE_ will be present.
|
||||||
|
* **PATCH**
|
||||||
|
* **FROM** - Version that the file last changed
|
||||||
|
* **PACKAGE** - Name of package that contains this file.
|
||||||
319
docs/file_formats/MEGAFEST.md
Normal file
319
docs/file_formats/MEGAFEST.md
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
> [!NOTE]
|
||||||
|
> DRAFT
|
||||||
|
|
||||||
|
# Megafest
|
||||||
|
|
||||||
|
The fat manifest for W3D Hub Packager and Launcher(s), represented as a JSON hash.
|
||||||
|
|
||||||
|
This **MEGAFEST** is intended to simplify patch generation and distribution by ensuring that
|
||||||
|
all the data the Packager and Launcher(s) need can be found in a **single** manifest file.
|
||||||
|
|
||||||
|
Speed up application packaging by only needing to scan and checksum the new builds files instead of needing
|
||||||
|
to maintain a pristine copy of the previous version to be diffed against.
|
||||||
|
|
||||||
|
Reduce the number of requests that the launcher(s) need to make to sort out how to download and
|
||||||
|
patch the application.
|
||||||
|
|
||||||
|
## Glossary
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
An individual file on disk.
|
||||||
|
|
||||||
|
An individual file as part of a MIX archive.
|
||||||
|
|
||||||
|
### Package
|
||||||
|
|
||||||
|
A collection of one or more files in a compressed archive for efficiently distributing the application.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Packages _MAY_ be compressed using ZSTANDARD (.zst) in the future. Do not assume all packages are zip files.
|
||||||
|
>
|
||||||
|
> Package names in the megafest include the file extension for this reason.
|
||||||
|
|
||||||
|
## Example `megafest.json` File
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"spec": 0,
|
||||||
|
"application": {
|
||||||
|
"id": "apb",
|
||||||
|
"version": "3.7.0.1",
|
||||||
|
"user_level": "public",
|
||||||
|
"previous_versions": [
|
||||||
|
"3.7.0.0"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
"msvc-2022"
|
||||||
|
],
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "binaries.zip",
|
||||||
|
"version": "3.7.0.0",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 4096,
|
||||||
|
"chunk_size": 4194304,
|
||||||
|
"chunk_checksums": [
|
||||||
|
"SHA256-HASH-A",
|
||||||
|
"SHA256-HASH-B"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "game2.exe",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 8691743
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.dat.zip",
|
||||||
|
"version": "3.7.0.0",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 1355917483,
|
||||||
|
"chunk_size": 4194304,
|
||||||
|
"chunk_checksums": [
|
||||||
|
"SHA256-HASH-A",
|
||||||
|
"SHA256-HASH-B"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "mp_wep_gdi.w3d",
|
||||||
|
"checksum": "CRC32-HASH",
|
||||||
|
"offset": 0,
|
||||||
|
"size": 8691743
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.patch.3.7.0.0.zst",
|
||||||
|
"version": "3.7.0.1",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 1355919483,
|
||||||
|
"chunk_size": 4194304,
|
||||||
|
"chunk_checksums": [
|
||||||
|
"SHA256-HASH-A",
|
||||||
|
"SHA256-HASH-B"
|
||||||
|
],
|
||||||
|
"from_version": "3.7.0.0",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "data/Always.patch",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 8691743
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "binaries.zip",
|
||||||
|
"version": "3.7.0.1",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 4096,
|
||||||
|
"chunk_size": 4194304,
|
||||||
|
"chunk_checksums": [
|
||||||
|
"SHA256-HASH-A",
|
||||||
|
"SHA256-HASH-B"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "game.exe",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 8691743
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"changes": [
|
||||||
|
{
|
||||||
|
"name": "game.exe",
|
||||||
|
"type": "added",
|
||||||
|
"package": "binaries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "game2.exe",
|
||||||
|
"type": "removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.dat",
|
||||||
|
"type": "updated",
|
||||||
|
"package": "Always.dat"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index": [
|
||||||
|
{
|
||||||
|
"name": "game.exe",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 8691743
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.dat",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 1355917483
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SPECIFICATION
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
List of application details:
|
||||||
|
|
||||||
|
* **ID** - Unique Application ID
|
||||||
|
* **VERSION** - Application Unique Version
|
||||||
|
* **USER_LEVEL** - User access level for this version
|
||||||
|
* **PREVIOUS_VERSIONS** - Complete list of previous versions. Full builds only include _the_ previous version
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "apb",
|
||||||
|
"version": "3.7.0.1",
|
||||||
|
"user_level": "public",
|
||||||
|
"previous_versions": [
|
||||||
|
"3.7.0.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
List of application's dependencies
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"msvc-2022"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Packages
|
||||||
|
|
||||||
|
Complete list of application's version tree of packages.
|
||||||
|
|
||||||
|
That is, every package from the last full build to present for this version series.
|
||||||
|
|
||||||
|
* **NAME** - Name of package
|
||||||
|
* **VERSION** - Application version this package belongs to
|
||||||
|
* **CHECKSUM** - SHA256 checksum of package
|
||||||
|
* **SIZE** - File size of package in bytes
|
||||||
|
* CHUNK **SIZE** - Size _CHUNK_CHECKSUMS_ represent in bytes
|
||||||
|
* **CHUNK_CHECKSUMS** - Array of file chunk checksums
|
||||||
|
* **FROM_VERSION** - *OPTIONAL* Version this file was last changed. Only present for MIX patches.
|
||||||
|
* **FILES** - Array of files
|
||||||
|
* **NAME**
|
||||||
|
* **CHECKSUM** - SHA256 checksum of file.
|
||||||
|
* **SIZE** - File size in bytes
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "binaries",
|
||||||
|
"version": "3.7.0.0",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 4096,
|
||||||
|
"chunk_size": 4194304,
|
||||||
|
"chunk_checksums": [
|
||||||
|
"SHA256-HASH-A",
|
||||||
|
"SHA256-HASH-B"
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "game.exe",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 8691743
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.patch.3.7.0.0",
|
||||||
|
"version": "3.7.0.1",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 2436543,
|
||||||
|
"chunk_size": 4194304,
|
||||||
|
"chunk_checksums": [
|
||||||
|
"SHA256-HASH-A",
|
||||||
|
"SHA256-HASH-B"
|
||||||
|
],
|
||||||
|
"from_version": "3.7.0.0",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "data/Always.patch",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 3636749
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
List of file changes in this build.
|
||||||
|
|
||||||
|
* **NAME** - Name of file
|
||||||
|
* **TYPE** - Type of change
|
||||||
|
* **ADDED** - New file has been added
|
||||||
|
* **UPDATED** - File has been changed
|
||||||
|
* **REMOVED** - File has been deleted
|
||||||
|
* **PACKAGE** - _OPTIONAL_ Name of package, for this build version, the file can be found in
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "game.exe",
|
||||||
|
"type": "updated",
|
||||||
|
"package": "binaries.zip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "game2.exe",
|
||||||
|
"type": "removed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.dat",
|
||||||
|
"type": "added",
|
||||||
|
"package": "Always.dat.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Index
|
||||||
|
|
||||||
|
Complete list of files for this build with: filename, checksum, file size, and list of files for MIX archives.
|
||||||
|
|
||||||
|
* **NAME** - File name
|
||||||
|
* **CHECKSUM** - SHA256 hash of file
|
||||||
|
* **SIZE** - File size
|
||||||
|
* **FILES** - _OPTIONAL_ List of files inside of MIX archives
|
||||||
|
* **NAME** - File name
|
||||||
|
* **CHECKSUM** - CRC32 hash of file data
|
||||||
|
* **OFFSET** - Offset of file in MIX archive
|
||||||
|
* **SIZE** - Size of file data
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "game.exe",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 8691743
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "data/Always.dat",
|
||||||
|
"checksum": "SHA256-HASH",
|
||||||
|
"size": 570961432,
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "mp_wep_gdi.w3d",
|
||||||
|
"checksum": "CRC32_HASH",
|
||||||
|
"offset": 0,
|
||||||
|
"size": 3524
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
132
docs/file_formats/MIX.md
Normal file
132
docs/file_formats/MIX.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Westwood Renegade MIX Archive
|
||||||
|
|
||||||
|
with W3D Hub / Tiberian Technologies alterations
|
||||||
|
|
||||||
|
The .MIX archive file format is used by Renegade as _yes_.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The MIX format has no notion about directories.
|
||||||
|
>
|
||||||
|
> Each file name **MUST** be unique.
|
||||||
|
>
|
||||||
|
> e.g. `data/map.dds` and `data/RA_Under/MAP.dds` have the same base file name
|
||||||
|
> and therefore **MUST** raise an error when writing.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `io_pos` is the current position on the seek head of the open file
|
||||||
|
|
||||||
|
## Reading
|
||||||
|
|
||||||
|
* Read [Header](#header)
|
||||||
|
* validate MIME type (`MIX1` or `MIX2`)
|
||||||
|
* Jump to io_pos `file_data_offset`
|
||||||
|
* Read `file_count`
|
||||||
|
* Read array of [`file_data`](#file-data-array)
|
||||||
|
* Jump to io_pos `file_names_offset`
|
||||||
|
* Read `file_count`
|
||||||
|
* Read array of [`file_names`](#file-names-array)
|
||||||
|
* Read file blobs
|
||||||
|
* For each file's [`file_data`](#file-data-array)
|
||||||
|
* Jump io_pos to `file_content_offset`
|
||||||
|
* Read `file_content_length`
|
||||||
|
* Done.
|
||||||
|
|
||||||
|
## Writing
|
||||||
|
|
||||||
|
* Write MIME type (`MIX1` or `MIX2`)
|
||||||
|
* Jump to io_pos `16`
|
||||||
|
* Write file blobs
|
||||||
|
* **IMPORTANT:** Add `-io_pos & 7` padding to io_pos **AFTER** each file blob is written
|
||||||
|
* Save io_pos as `file_data_offset`
|
||||||
|
* Write `file_count`
|
||||||
|
* Write array of [`file_data`](#file-data-array)
|
||||||
|
* Save io_pos as `file_names_offset`
|
||||||
|
* Write `file_count`
|
||||||
|
* Write array of [`file_names`](#file-names-array)
|
||||||
|
* **IMPORTANT:** Ensure file names are null terminated and DO NOT exceed 254 (+ null byte) characters in length.
|
||||||
|
* Jump to io_pos `4`
|
||||||
|
* Write [`file_data_offset`](#header)
|
||||||
|
* Write [`file_names_offset`](#header)
|
||||||
|
* Write [`reserved`](#header)
|
||||||
|
* Done.
|
||||||
|
|
||||||
|
## Pseudo Example:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
header:
|
||||||
|
mime: MIX1 or MIX2
|
||||||
|
file_data_offset: 0x1024EAEA
|
||||||
|
file_names_offset: 0x8192EAEA
|
||||||
|
reserved: 0
|
||||||
|
|
||||||
|
files:
|
||||||
|
array:
|
||||||
|
- file_blob
|
||||||
|
|
||||||
|
file_data:
|
||||||
|
file_count: 0x000021
|
||||||
|
array:
|
||||||
|
file_name_crc32: 0xEFEFEFEF
|
||||||
|
file_content_offset: 0x3217DEAD
|
||||||
|
file_content_length: 0x0018BEEF
|
||||||
|
|
||||||
|
file_names:
|
||||||
|
file_count: 0x000021
|
||||||
|
array:
|
||||||
|
file_name_length: 0xff
|
||||||
|
file_name: "mp_wep_gdi.w3d"
|
||||||
|
```
|
||||||
|
|
||||||
|
## MIME Types
|
||||||
|
|
||||||
|
### MIX1 `0x3158494D`
|
||||||
|
|
||||||
|
Westwood Farm Fresh Organic.
|
||||||
|
|
||||||
|
### MIX2 `0x3258494D`
|
||||||
|
|
||||||
|
Same as `MIX1`. `MIX2` hints to the engine's file reader to decrypt files before use.
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
|
||||||
|
### Header
|
||||||
|
|
||||||
|
| Name | Offset | Type | Description |
|
||||||
|
|-------------------|--------|---------|---------------------------------------------------------------------|
|
||||||
|
| MIME | 0 | int32_t | 4 bytes representing `MIX1` (`0x3158494D)` or `MIX2` (`0x3258494D`) |
|
||||||
|
| File Data Offset | 4 | int32_t | Offset in MIX archive that [`file_data`](#file-data) data starts |
|
||||||
|
| File Names Offset | 8 | int32_t | Offset in MIX archive that [`file_name`](#file-names) data starts |
|
||||||
|
| RESERVED | 12 | int32_t | Unused reserved int. Write as `0` |
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
| Name | Offset | Type | Description |
|
||||||
|
|-----------|---------|-----------------------------|---------------|
|
||||||
|
| File Blob | complex | char[`file_content_length`] | File contents |
|
||||||
|
|
||||||
|
### File Data
|
||||||
|
|
||||||
|
| Name | Offset | Type | Description |
|
||||||
|
|------------|--------------------|---------|--------------------------------|
|
||||||
|
| File Count | `file_data_offset` | int32_t | Number of files in MIX archive |
|
||||||
|
|
||||||
|
### File Data Array
|
||||||
|
|
||||||
|
| Name | Offset | Type | Description |
|
||||||
|
|---------------------|--------------------------------------------|----------|--------------------------------------------------------|
|
||||||
|
| File Name CRC32 | `file_data_offset` + 4 * (index * sizeof) | uint32_t | CRC32 of **UPPERCASE** file name in network byte order |
|
||||||
|
| File Content Offset | `file_data_offset` + 8 * (index * sizeof) | uint32_t | File content offset |
|
||||||
|
| File Content Length | `file_data_offset` + 12 * (index * sizeof) | uint32_t | File content length |
|
||||||
|
|
||||||
|
### File Names
|
||||||
|
|
||||||
|
| Name | Offset | Type | Description |
|
||||||
|
|------------|---------------------|---------|--------------------------------|
|
||||||
|
| File Count | `file_names_offset` | int32_t | Number of files in MIX archive |
|
||||||
|
|
||||||
|
### File Names Array
|
||||||
|
|
||||||
|
| Name | Offset | Type | Description |
|
||||||
|
|------------------|---------|--------------------------|-------------------------------------------------|
|
||||||
|
| File Name Length | complex | uint8_t | Length of string in bytes, including null byte. |
|
||||||
|
| File Name | complex | char[`file_name_length`] | null terminated string |
|
||||||
19
docs/file_formats/W3D_HUB_PATCH.md
Normal file
19
docs/file_formats/W3D_HUB_PATCH.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# W3D Hub Patch
|
||||||
|
A W3D Hub Patch
|
||||||
|
|
||||||
|
A `.w3dhub.patch` is included in MIX Patch files.
|
||||||
|
It contains instructions for updating the target MIX.
|
||||||
|
|
||||||
|
JSON file with a static CRC32 of `E6FE46B8`
|
||||||
|
|
||||||
|
## Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"removedFiles": [
|
||||||
|
"sov_v_mad.gif"
|
||||||
|
],
|
||||||
|
"updatedFiles": [
|
||||||
|
"sov_v_mad.w3d"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
36
docs/protocols/LAN_DISCOVERY.md
Normal file
36
docs/protocols/LAN_DISCOVERY.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
> [!NOTE]
|
||||||
|
> DRAFT
|
||||||
|
|
||||||
|
# LAN Discovery
|
||||||
|
|
||||||
|
Doing things
|
||||||
|
|
||||||
|
Broadcast port: 4898
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
|-------------------|----------|--------------------------------------------------------------------------|
|
||||||
|
| Version | int32_t | Version of protocol this application supports |
|
||||||
|
| Owner | string | Nickname of application's active user |
|
||||||
|
| Hostname | string | Name of device |
|
||||||
|
| UUID | string | Unique identifier for this application |
|
||||||
|
| Application | string | Name of application |
|
||||||
|
| Features | array | Array of strings naming features the application supports |
|
||||||
|
| Service Port | uint16_t | Dynamically assigned TCP port that interested parties should connect too |
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Max packet size for UDP broadcast may need to be limited to 512 bytes
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 0,
|
||||||
|
"owner": "cyberarm",
|
||||||
|
"hostname": "PC-1692",
|
||||||
|
"uuid": "019bcf1e-a22e-7fe0-a3db-3a9d37bfc6fa",
|
||||||
|
"application": "W3D Hub Linux Launcher",
|
||||||
|
"features": [
|
||||||
|
"launcher_remote:3",
|
||||||
|
"package_share:1"
|
||||||
|
],
|
||||||
|
"service_port": 56802
|
||||||
|
}
|
||||||
|
```
|
||||||
9
docs/protocols/LAN_PACKAGE_SHARE.md
Normal file
9
docs/protocols/LAN_PACKAGE_SHARE.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
> [!NOTE]
|
||||||
|
> DRAFT
|
||||||
|
|
||||||
|
# LAN Package Share
|
||||||
|
|
||||||
|
Authentication is not required, only user opt-in to enable package sharing from their application.
|
||||||
|
|
||||||
|
* List available packages
|
||||||
|
* upload or download verified packages between other applications on LAN network
|
||||||
9
docs/protocols/LAUNCHER_REMOTE.md
Normal file
9
docs/protocols/LAUNCHER_REMOTE.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
> [!NOTE]
|
||||||
|
> DRAFT
|
||||||
|
|
||||||
|
# Launcher Remote
|
||||||
|
|
||||||
|
* Get list of available, installed, and in progress applications
|
||||||
|
* Trigger download or update of application
|
||||||
|
* Delete application
|
||||||
|
* Join server
|
||||||
@@ -60,15 +60,15 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Handle arbitrary urls that may come through
|
# Handle arbitrary urls that may come through
|
||||||
url = nil
|
|
||||||
if path.start_with?("http")
|
if path.start_with?("http")
|
||||||
uri = URI(path)
|
uri = URI(path)
|
||||||
|
|
||||||
endpoint = uri.origin
|
endpoint = uri.origin
|
||||||
path = uri.request_uri
|
path = uri.request_uri
|
||||||
else
|
|
||||||
url = "#{endpoint}#{path}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
url = "#{endpoint}#{path}"
|
||||||
|
|
||||||
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
|
logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
|
||||||
|
|
||||||
# Inject Authorization header if account data is populated
|
# Inject Authorization header if account data is populated
|
||||||
|
|||||||
@@ -20,8 +20,12 @@ class W3DHub
|
|||||||
|
|
||||||
@status = Status.new(@data[:status])
|
@status = Status.new(@data[:status])
|
||||||
|
|
||||||
@ping_interval = 30_000
|
# if we're on unix and using the PingManager then check every second since
|
||||||
|
# we're not _actually_ pinging the server.
|
||||||
|
@ping_interval = W3DHub.unix? ? 1_000 : 60_000
|
||||||
@last_pinged = Gosu.milliseconds + @ping_interval + 1_000
|
@last_pinged = Gosu.milliseconds + @ping_interval + 1_000
|
||||||
|
|
||||||
|
Store.ping_manager.add_address(@address)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(hash)
|
def update(hash)
|
||||||
@@ -49,6 +53,16 @@ class W3DHub
|
|||||||
if force_ping || Gosu.milliseconds - @last_pinged >= @ping_interval
|
if force_ping || Gosu.milliseconds - @last_pinged >= @ping_interval
|
||||||
@last_pinged = Gosu.milliseconds
|
@last_pinged = Gosu.milliseconds
|
||||||
|
|
||||||
|
if W3DHub.unix?
|
||||||
|
average_ping = Store.ping_manager.ping_for(@address)
|
||||||
|
|
||||||
|
@ping = average_ping.negative? ? NO_OR_BAD_PING : average_ping
|
||||||
|
|
||||||
|
States::Interface.instance&.update_server_ping(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
return unless W3DHub.windows?
|
||||||
|
|
||||||
W3DHub::BackgroundWorker.foreground_parallel_job(
|
W3DHub::BackgroundWorker.foreground_parallel_job(
|
||||||
lambda do
|
lambda do
|
||||||
W3DHub.command("ping #{@address} #{W3DHub.windows? ? '-n 3' : '-c 3'}") do |line|
|
W3DHub.command("ping #{@address} #{W3DHub.windows? ? '-n 3' : '-c 3'}") do |line|
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ class W3DHub
|
|||||||
# unpack packages
|
# unpack packages
|
||||||
# install dependencies (e.g. visual C runtime)
|
# install dependencies (e.g. visual C runtime)
|
||||||
|
|
||||||
@tasks.push(Installer.new(app_id, channel))
|
installer = Installer.new(app_id, channel)
|
||||||
|
|
||||||
|
@tasks.push(installer)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update(app_id, channel)
|
def update(app_id, channel)
|
||||||
@@ -31,7 +33,9 @@ class W3DHub
|
|||||||
|
|
||||||
return false unless installed?(app_id, channel)
|
return false unless installed?(app_id, channel)
|
||||||
|
|
||||||
@tasks.push(Updater.new(app_id, channel))
|
updater = Updater.new(app_id, channel)
|
||||||
|
|
||||||
|
@tasks.push(updater)
|
||||||
end
|
end
|
||||||
|
|
||||||
def import(app_id, channel)
|
def import(app_id, channel)
|
||||||
@@ -174,6 +178,7 @@ class W3DHub
|
|||||||
return vars if W3DHub.windows?
|
return vars if W3DHub.windows?
|
||||||
|
|
||||||
vars["WINEPREFIX"] = Store.settings[:wine_prefix] unless Store.settings[:wine_prefix].to_s.empty?
|
vars["WINEPREFIX"] = Store.settings[:wine_prefix] unless Store.settings[:wine_prefix].to_s.empty?
|
||||||
|
# vars["WINEDEBUG"] = "-all" if true # TODO make this an option. wine debug interferences with pid returned from Process.spawn
|
||||||
|
|
||||||
vars
|
vars
|
||||||
end
|
end
|
||||||
@@ -243,7 +248,6 @@ class W3DHub
|
|||||||
wine_enviroment_variables(app_id, channel)
|
wine_enviroment_variables(app_id, channel)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
attempted = false
|
attempted = false
|
||||||
begin
|
begin
|
||||||
pid = Process.spawn(
|
pid = Process.spawn(
|
||||||
@@ -255,6 +259,7 @@ class W3DHub
|
|||||||
"-launcher #{args.join(' ')}"
|
"-launcher #{args.join(' ')}"
|
||||||
)
|
)
|
||||||
Process.detach(pid)
|
Process.detach(pid)
|
||||||
|
BackgroundWorker.foreground_parallel_job(-> { monitor_process(app_id, channel, pid) }, ->(result) { handle_process_result(app_id, channel, result) })
|
||||||
rescue Errno::EINVAL => e
|
rescue Errno::EINVAL => e
|
||||||
retryable = !attempted
|
retryable = !attempted
|
||||||
attempted = true
|
attempted = true
|
||||||
@@ -267,14 +272,51 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def monitor_process(app_id, channel, pid)
|
||||||
|
key = "#{app_id}-#{channel}"
|
||||||
|
@running_applications[key] = pid
|
||||||
|
|
||||||
|
status = Process::Status.wait(pid)
|
||||||
|
pp [pid, status]
|
||||||
|
|
||||||
|
@running_applications.delete(key)
|
||||||
|
|
||||||
|
status
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_process_result(app_id, channel, status)
|
||||||
|
pp [app_id, channel, status]
|
||||||
|
|
||||||
|
# Everything's fine
|
||||||
|
return if status.pid >= 0 && status.success?
|
||||||
|
|
||||||
|
# Everything's not fine
|
||||||
|
reason = status.pid.positive? ? "Crashed" : "Failed to Launch"
|
||||||
|
game = Store.applications.games.find { |g| g.id == app_id }
|
||||||
|
title = "#{reason}: #{game.name}" if game
|
||||||
|
title = "Application #{reason}" unless game
|
||||||
|
|
||||||
|
message = if status.pid.negative?
|
||||||
|
"Command Not Found."
|
||||||
|
else
|
||||||
|
"Application crashed."
|
||||||
|
end
|
||||||
|
|
||||||
|
push_state(
|
||||||
|
States::MessageDialog,
|
||||||
|
title: title,
|
||||||
|
message: message,
|
||||||
|
accept_callback: proc {
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false)
|
def join_server(app_id, channel, server, username = Store.settings[:server_list_username], password = nil, multi = false)
|
||||||
return unless installed?(app_id, channel) && username.to_s.length.positive?
|
return unless installed?(app_id, channel) && username.to_s.length.positive?
|
||||||
|
|
||||||
run(
|
run(
|
||||||
app_id, channel,
|
app_id, channel,
|
||||||
"+connect #{server.address}:#{server.port} "\
|
"+connect #{server.address}:#{server.port} +netplayername #{username}#{password ? " +password \"#{password}\"" : ""}#{multi ? " +multi" : ""}"
|
||||||
"+netplayername #{username}#{password ? " +password \"#{password}\"" : ""}"\
|
|
||||||
"#{multi ? " +multi" : ""}"
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -642,8 +642,7 @@ class W3DHub
|
|||||||
verified = verify_package(package)
|
verified = verify_package(package)
|
||||||
|
|
||||||
# download manifest if not valid
|
# download manifest if not valid
|
||||||
package_fetch(package) unless verified
|
verified ? true : package_fetch(package)
|
||||||
true if verified
|
|
||||||
else
|
else
|
||||||
# download manifest if not cached
|
# download manifest if not cached
|
||||||
package_fetch(package)
|
package_fetch(package)
|
||||||
@@ -657,9 +656,10 @@ class W3DHub
|
|||||||
block&.call(chunk, remaining_bytes, total_bytes)
|
block&.call(chunk, remaining_bytes, total_bytes)
|
||||||
end
|
end
|
||||||
|
|
||||||
fail!("Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})") unless status_okay
|
return status_okay if status_okay
|
||||||
|
|
||||||
status_okay
|
fail!("Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})")
|
||||||
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_package(package, &block)
|
def verify_package(package, &block)
|
||||||
@@ -746,8 +746,6 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") }
|
patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") }
|
||||||
patch_entry.read
|
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, symbolize_names: true)
|
patch_info = JSON.parse(patch_entry.blob, symbolize_names: true)
|
||||||
|
|
||||||
@@ -767,15 +765,20 @@ class W3DHub
|
|||||||
patch_info[:updatedFiles].each do |file|
|
patch_info[:updatedFiles].each do |file|
|
||||||
logger.debug(LOG_TAG) { " #{file}" }
|
logger.debug(LOG_TAG) { " #{file}" }
|
||||||
|
|
||||||
patch_mix.entries.each do |entry|
|
patch = patch_mix.entries.find { |e| e.name.casecmp?(file) }
|
||||||
target_mix.add_entry(entry: entry, replace: true)
|
target = target_mix.entries.find { |e| e.name.casecmp?(file) }
|
||||||
|
|
||||||
|
if target
|
||||||
|
target_mix.entries[target_mix.entries.index(target)] = patch
|
||||||
|
else
|
||||||
|
target_mix.entries << patch
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
|
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
|
||||||
temp_mix_path = "#{temp_path}/#{File.basename(file_path)}"
|
temp_mix_path = "#{temp_path}/#{File.basename(file_path)}"
|
||||||
temp_mix = W3DHub::WWMix.new(path: temp_mix_path, encrypted: target_mix.encrypted?)
|
temp_mix = W3DHub::WWMix.new(path: temp_mix_path)
|
||||||
target_mix.entries.each { |e| temp_mix.add_entry(entry: e, replace: true) }
|
target_mix.entries.each { |e| temp_mix.add_entry(entry: e) }
|
||||||
unless temp_mix.save
|
unless temp_mix.save
|
||||||
raise temp_mix.error_reason
|
raise temp_mix.error_reason
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def save_config(config = @config)
|
def save_config(config = @config)
|
||||||
File.write(CONFIG_PATH, JSON.pretty_generate(config))
|
File.write(CONFIG_PATH, config.to_json)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -90,8 +90,6 @@ class W3DHub
|
|||||||
result = true
|
result = true
|
||||||
end
|
end
|
||||||
|
|
||||||
binding.irb unless response
|
|
||||||
|
|
||||||
if response&.status == 200 || response&.status == 206
|
if response&.status == 200 || response&.status == 206
|
||||||
result = true
|
result = true
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -54,49 +54,51 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.prompt_for_nickname(accept_callback: nil, cancel_callback: nil)
|
def self.prompt_for_nickname(accept_callback: nil, cancel_callback: nil)
|
||||||
CyberarmEngine::Window.instance.push_state(
|
CyberarmEngine::Window.instance.push_state(
|
||||||
W3DHub::States::PromptDialog,
|
W3DHub::States::PromptDialog,
|
||||||
title: I18n.t(:"server_browser.set_nickname"),
|
title: I18n.t(:"server_browser.set_nickname"),
|
||||||
message: I18n.t(:"server_browser.set_nickname_message"),
|
message: I18n.t(:"server_browser.set_nickname_message"),
|
||||||
prefill: Store.settings[:server_list_username],
|
prefill: Store.settings[:server_list_username],
|
||||||
accept_callback: accept_callback,
|
accept_callback: accept_callback,
|
||||||
cancel_callback: cancel_callback,
|
cancel_callback: cancel_callback,
|
||||||
valid_callback: proc do |entry|
|
# See: https://gitlab.com/danpaul88/brenbot/-/blob/master/Source/renlog.pm#L136-175
|
||||||
entry.length.between?(3, 40) && (entry =~ /^[a-z0-9_\-\[\]]+$/i)
|
valid_callback: proc do |entry|
|
||||||
|
entry.length > 1 && entry.length < 30 && (entry =~ /(:|!|&|%| )/i).nil? &&
|
||||||
|
(entry =~ /[\001\002\037]/).nil? && (entry =~ /\\/).nil?
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.prompt_for_password(accept_callback: nil, cancel_callback: nil)
|
def self.prompt_for_password(accept_callback: nil, cancel_callback: nil)
|
||||||
CyberarmEngine::Window.instance.push_state(
|
CyberarmEngine::Window.instance.push_state(
|
||||||
W3DHub::States::PromptDialog,
|
W3DHub::States::PromptDialog,
|
||||||
title: I18n.t(:"server_browser.enter_password"),
|
title: I18n.t(:"server_browser.enter_password"),
|
||||||
message: I18n.t(:"server_browser.enter_password_message"),
|
message: I18n.t(:"server_browser.enter_password_message"),
|
||||||
input_type: :password,
|
input_type: :password,
|
||||||
accept_callback: accept_callback,
|
accept_callback: accept_callback,
|
||||||
cancel_callback: cancel_callback,
|
cancel_callback: cancel_callback,
|
||||||
valid_callback: proc { |entry| entry.length.positive? }
|
valid_callback: proc { |entry| entry.length.positive? }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.join_server(server:, username: Store.settings[:server_list_username], password: nil, multi: false)
|
def self.join_server(server:, username: Store.settings[:server_list_username], password: nil, multi: false)
|
||||||
if (
|
if (
|
||||||
(server.status.password && password.length.positive?) ||
|
(server.status.password && password.length.positive?) ||
|
||||||
!server.status.password) &&
|
!server.status.password) &&
|
||||||
username.to_s.length.positive?
|
username.to_s.length.positive?
|
||||||
|
|
||||||
Store.application_manager.join_server(
|
Store.application_manager.join_server(
|
||||||
server.game,
|
server.game,
|
||||||
server.channel,
|
server.channel,
|
||||||
server,
|
server,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
multi
|
multi
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
|
CyberarmEngine::Window.instance.push_state(W3DHub::States::MessageDialog, type: "?", title: "?", message: "?")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.command(command, &block)
|
def self.command(command, &block)
|
||||||
if windows?
|
if windows?
|
||||||
@@ -121,6 +123,7 @@ class W3DHub
|
|||||||
process_info = Process.create(**hash)
|
process_info = Process.create(**hash)
|
||||||
|
|
||||||
pid = process_info.process_id
|
pid = process_info.process_id
|
||||||
|
status = -1
|
||||||
|
|
||||||
until (status = Process.get_exitcode(pid))
|
until (status = Process.get_exitcode(pid))
|
||||||
if block
|
if block
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ class W3DHub
|
|||||||
class HardwareSurvey
|
class HardwareSurvey
|
||||||
attr_reader :data
|
attr_reader :data
|
||||||
|
|
||||||
def initialize(displays_only: false)
|
def initialize
|
||||||
@data = {
|
@data = {
|
||||||
displays: [],
|
displays: [],
|
||||||
system: {
|
system: {
|
||||||
@@ -26,6 +26,8 @@ class W3DHub
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hardware survey only works on Windows atm
|
||||||
|
|
||||||
if Gem::win_platform?
|
if Gem::win_platform?
|
||||||
lib_dir = File.dirname($LOADED_FEATURES.find { |file| file.include?("gosu.so") })
|
lib_dir = File.dirname($LOADED_FEATURES.find { |file| file.include?("gosu.so") })
|
||||||
SDL.load_lib("#{lib_dir}64/SDL2.dll")
|
SDL.load_lib("#{lib_dir}64/SDL2.dll")
|
||||||
@@ -34,13 +36,11 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
query_displays
|
query_displays
|
||||||
unless displays_only
|
query_motherboard
|
||||||
query_motherboard
|
query_operating_system
|
||||||
query_operating_system
|
query_cpus
|
||||||
query_cpus
|
query_ram
|
||||||
query_ram
|
query_gpus
|
||||||
query_gpus
|
|
||||||
end
|
|
||||||
|
|
||||||
@data.freeze
|
@data.freeze
|
||||||
end
|
end
|
||||||
@@ -68,51 +68,37 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def query_motherboard
|
def query_motherboard
|
||||||
if Gem::win_platform?
|
return unless Gem::win_platform?
|
||||||
begin
|
|
||||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("HARDWARE\\DESCRIPTION\\System\\BIOS", Win32::Registry::KEY_READ) do |reg|
|
Win32::Registry::HKEY_LOCAL_MACHINE.open("HARDWARE\\DESCRIPTION\\System\\BIOS", Win32::Registry::KEY_READ) do |reg|
|
||||||
@data[:system][:motherboard][:manufacturer] = safe_reg(reg, "SystemManufacturer")
|
@data[:system][:motherboard][:manufacturer] = safe_reg(reg, "SystemManufacturer")
|
||||||
@data[:system][:motherboard][:model] = safe_reg(reg, "SystemProductName")
|
@data[:system][:motherboard][:model] = safe_reg(reg, "SystemProductName")
|
||||||
@data[:system][:motherboard][:bios_vendor] = safe_reg(reg, "BIOSVendor")
|
@data[:system][:motherboard][:bios_vendor] = safe_reg(reg, "BIOSVendor")
|
||||||
@data[:system][:motherboard][:bios_release_date] = safe_reg(reg, "BIOSReleaseDate")
|
@data[:system][:motherboard][:bios_release_date] = safe_reg(reg, "BIOSReleaseDate")
|
||||||
@data[:system][:motherboard][:bios_version] = safe_reg(reg, "BIOSVersion")
|
@data[:system][:motherboard][:bios_version] = safe_reg(reg, "BIOSVersion")
|
||||||
end
|
|
||||||
rescue Win32::Registry::Error
|
|
||||||
@data[:system][:motherboard][:manufacturer] = "Unknown"
|
|
||||||
@data[:system][:motherboard][:model] = "Unknown"
|
|
||||||
@data[:system][:motherboard][:bios_vendor] = "Unknown"
|
|
||||||
@data[:system][:motherboard][:bios_release_date] = "Unknown"
|
|
||||||
@data[:system][:motherboard][:bios_version] = "Unknown"
|
|
||||||
end
|
|
||||||
else # unix
|
|
||||||
@data[:system][:motherboard][:manufacturer] = safe_file("/sys/devices/virtual/dmi/id/board_vendor")
|
|
||||||
@data[:system][:motherboard][:model] = safe_file("/sys/devices/virtual/dmi/id/board_name")
|
|
||||||
@data[:system][:motherboard][:bios_version] = safe_file("/sys/devices/virtual/dmi/id/board_version")
|
|
||||||
end
|
end
|
||||||
|
rescue Win32::Registry::Error
|
||||||
|
@data[:system][:motherboard][:manufacturer] = "Unknown"
|
||||||
|
@data[:system][:motherboard][:model] = "Unknown"
|
||||||
|
@data[:system][:motherboard][:bios_vendor] = "Unknown"
|
||||||
|
@data[:system][:motherboard][:bios_release_date] = "Unknown"
|
||||||
|
@data[:system][:motherboard][:bios_version] = "Unknown"
|
||||||
end
|
end
|
||||||
|
|
||||||
def query_operating_system
|
def query_operating_system
|
||||||
if Gem::win_platform?
|
return unless Gem::win_platform?
|
||||||
begin
|
|
||||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Win32::Registry::KEY_READ) do |reg|
|
Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", Win32::Registry::KEY_READ) do |reg|
|
||||||
@data[:system][:operating_system][:name] = safe_reg(reg, "ProductName")
|
@data[:system][:operating_system][:name] = safe_reg(reg, "ProductName")
|
||||||
@data[:system][:operating_system][:build] = safe_reg(reg, "CurrentBuild")
|
@data[:system][:operating_system][:build] = safe_reg(reg, "CurrentBuild")
|
||||||
@data[:system][:operating_system][:version] = safe_reg(reg, "DisplayVersion")
|
@data[:system][:operating_system][:version] = safe_reg(reg, "DisplayVersion")
|
||||||
@data[:system][:operating_system][:edition] = safe_reg(reg, "EditionID")
|
@data[:system][:operating_system][:edition] = safe_reg(reg, "EditionID")
|
||||||
end
|
|
||||||
rescue Win32::Registry::Error
|
|
||||||
@data[:system][:operating_system][:name] = "Unknown"
|
|
||||||
@data[:system][:operating_system][:build] = "Unknown"
|
|
||||||
@data[:system][:operating_system][:version] = "Unknown"
|
|
||||||
@data[:system][:operating_system][:edition] = "Unknown"
|
|
||||||
end
|
|
||||||
else # unix
|
|
||||||
release_info = query_release_info
|
|
||||||
@data[:system][:operating_system][:name] = release_info["pretty_name"] || release_info["name"] || "Unknown"
|
|
||||||
@data[:system][:operating_system][:build] = release_info["version_codename"] || release_info["build_id"] || "Unknown"
|
|
||||||
@data[:system][:operating_system][:version] = release_info["version_id"] || release_info["build_id"] || "Unknown"
|
|
||||||
@data[:system][:operating_system][:edition] = release_info["id"] || release_info["id_like"] || "Unknown"
|
|
||||||
end
|
end
|
||||||
|
rescue Win32::Registry::Error
|
||||||
|
@data[:system][:operating_system][:name] = "Unknown"
|
||||||
|
@data[:system][:operating_system][:build] = "Unknown"
|
||||||
|
@data[:system][:operating_system][:version] = "Unknown"
|
||||||
|
@data[:system][:operating_system][:edition] = "Unknown"
|
||||||
end
|
end
|
||||||
|
|
||||||
def query_cpus
|
def query_cpus
|
||||||
@@ -136,16 +122,6 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
rescue Win32::Registry::Error
|
rescue Win32::Registry::Error
|
||||||
end
|
end
|
||||||
else
|
|
||||||
cpu_info = query_cpu_info
|
|
||||||
cpu_info.each do |cpu|
|
|
||||||
@data[:system][:cpus] << {
|
|
||||||
manufacturer: cpu["manufacturer"] || "Unknown",
|
|
||||||
model: cpu["model"] || "Unknown",
|
|
||||||
mhz: cpu["mhz"] || "Unknown",
|
|
||||||
family: cpu["family"] || "Unknown"
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
instruction_sets = %w[ HasRDTSC HasAltiVec HasMMX Has3DNow HasSSE HasSSE2 HasSSE3 HasSSE41 HasSSE42 HasAVX HasAVX2 HasAVX512F HasARMSIMD HasNEON ] # HasLSX HasLASX # These cause a crash atm
|
instruction_sets = %w[ HasRDTSC HasAltiVec HasMMX Has3DNow HasSSE HasSSE2 HasSSE3 HasSSE41 HasSSE42 HasAVX HasAVX2 HasAVX512F HasARMSIMD HasNEON ] # HasLSX HasLASX # These cause a crash atm
|
||||||
@@ -164,57 +140,44 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def query_gpus
|
def query_gpus
|
||||||
if Gem::win_platform?
|
return unless Gem::win_platform?
|
||||||
begin
|
|
||||||
Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}", Win32::Registry::KEY_READ) do |reg|
|
|
||||||
i = 0
|
|
||||||
|
|
||||||
reg.each_key do |key, _|
|
Win32::Registry::HKEY_LOCAL_MACHINE.open("SYSTEM\\ControlSet001\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}", Win32::Registry::KEY_READ) do |reg|
|
||||||
next unless key.start_with?("0")
|
i = 0
|
||||||
|
|
||||||
reg.open(key) do |device|
|
reg.each_key do |key, _|
|
||||||
|
next unless key.start_with?("0")
|
||||||
|
|
||||||
|
reg.open(key) do |device|
|
||||||
|
vram = -1
|
||||||
|
|
||||||
|
begin
|
||||||
|
vram = device["HardwareInformation.qwMemorySize"].to_i
|
||||||
|
rescue Win32::Registry::Error, TypeError
|
||||||
|
begin
|
||||||
|
vram = device["HardwareInformation.MemorySize"].to_i
|
||||||
|
rescue Win32::Registry::Error, TypeError
|
||||||
vram = -1
|
vram = -1
|
||||||
|
|
||||||
begin
|
|
||||||
vram = device["HardwareInformation.qwMemorySize"].to_i
|
|
||||||
rescue Win32::Registry::Error, TypeError
|
|
||||||
begin
|
|
||||||
vram = device["HardwareInformation.MemorySize"].to_i
|
|
||||||
rescue Win32::Registry::Error, TypeError
|
|
||||||
vram = -1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
next if vram.negative?
|
|
||||||
|
|
||||||
vram = vram / 1024.0 / 1024.0
|
|
||||||
|
|
||||||
@data[:system][:gpus] << {
|
|
||||||
manufacturer: safe_reg(device, "ProviderName"),
|
|
||||||
model: safe_reg(device, "DriverDesc"),
|
|
||||||
vram: vram.round,
|
|
||||||
driver_date: safe_reg(device, "DriverDate"),
|
|
||||||
driver_version: safe_reg(device, "DriverVersion")
|
|
||||||
}
|
|
||||||
|
|
||||||
i += 1
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
next if vram.negative?
|
||||||
|
|
||||||
|
vram = vram / 1024.0 / 1024.0
|
||||||
|
|
||||||
|
@data[:system][:gpus] << {
|
||||||
|
manufacturer: safe_reg(device, "ProviderName"),
|
||||||
|
model: safe_reg(device, "DriverDesc"),
|
||||||
|
vram: vram.round,
|
||||||
|
driver_date: safe_reg(device, "DriverDate"),
|
||||||
|
driver_version: safe_reg(device, "DriverVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1
|
||||||
end
|
end
|
||||||
rescue Win32::Registry::Error
|
|
||||||
end
|
|
||||||
else # unix
|
|
||||||
gpu_info = query_glx_info
|
|
||||||
gpu_info.each do |gpu|
|
|
||||||
@data[:system][:gpus] << {
|
|
||||||
manufacturer: gpu["manufacturer"] || "Unknown",
|
|
||||||
model: gpu["model"] || "Unknown",
|
|
||||||
vram: gpu["vram"].to_i,
|
|
||||||
driver_date: gpu["driver_date"] || "Unknown",
|
|
||||||
driver_version: gpu["driver_version"] || "Unknown"
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
rescue Win32::Registry::Error
|
||||||
end
|
end
|
||||||
|
|
||||||
def safe_reg(reg, key, default_value = "Unknown")
|
def safe_reg(reg, key, default_value = "Unknown")
|
||||||
@@ -222,130 +185,5 @@ class W3DHub
|
|||||||
rescue Win32::Registry::Error
|
rescue Win32::Registry::Error
|
||||||
default_value
|
default_value
|
||||||
end
|
end
|
||||||
|
|
||||||
def safe_file(path, default_value = "Unknown")
|
|
||||||
value = File.read(path).to_s.strip
|
|
||||||
return default_value if value.downcase == "default string"
|
|
||||||
|
|
||||||
value
|
|
||||||
rescue
|
|
||||||
default_value
|
|
||||||
end
|
|
||||||
|
|
||||||
def query_release_info
|
|
||||||
hash = {}
|
|
||||||
|
|
||||||
File.open("/etc/os-release") do |f|
|
|
||||||
f.each_line do |line|
|
|
||||||
line = line.strip
|
|
||||||
|
|
||||||
key, value = line.split("=", 2)
|
|
||||||
value.gsub!('"', "")
|
|
||||||
|
|
||||||
hash[key.downcase] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
hash
|
|
||||||
rescue
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def query_cpu_info
|
|
||||||
cpus = []
|
|
||||||
|
|
||||||
cpu = {}
|
|
||||||
File.open("/proc/cpuinfo") do |f|
|
|
||||||
f.each_line do |line|
|
|
||||||
line = line.strip
|
|
||||||
|
|
||||||
if line.empty?
|
|
||||||
cpu["family"] = format(
|
|
||||||
"%s Family %s Model %s Stepping %s",
|
|
||||||
cpu["manufacturer"] || "Unknown",
|
|
||||||
cpu["_family"] || "Unknown",
|
|
||||||
cpu["_model"] || "Unknown",
|
|
||||||
cpu["_stepping"] || "Unknown",
|
|
||||||
)
|
|
||||||
|
|
||||||
cpus << cpu
|
|
||||||
cpu = {}
|
|
||||||
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
key, value = line.split(":", 2).map(&:strip)
|
|
||||||
|
|
||||||
case key.downcase
|
|
||||||
when "vendor_id"
|
|
||||||
cpu["manufacturer"] = value
|
|
||||||
when "model name"
|
|
||||||
cpu["model"] = value
|
|
||||||
when "cpu mhz"
|
|
||||||
cpu["mhz"] = value
|
|
||||||
|
|
||||||
when "cpu family"
|
|
||||||
cpu["_family"] = value
|
|
||||||
when "model"
|
|
||||||
cpu["_model"] = value
|
|
||||||
when "stepping"
|
|
||||||
cpu["_stepping"] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
cpus
|
|
||||||
rescue
|
|
||||||
cpus
|
|
||||||
end
|
|
||||||
|
|
||||||
def query_glx_info
|
|
||||||
gpus = []
|
|
||||||
glxinfo = `glxinfo`
|
|
||||||
|
|
||||||
return gpus if glxinfo.empty?
|
|
||||||
|
|
||||||
gpu = {}
|
|
||||||
glxinfo.lines do |line|
|
|
||||||
line = line.strip
|
|
||||||
|
|
||||||
next if line.empty?
|
|
||||||
|
|
||||||
key, value = line.split(":", 2).map(&:strip)
|
|
||||||
|
|
||||||
mesa_info = false
|
|
||||||
gpu_memory_info = false
|
|
||||||
case key.downcase
|
|
||||||
when "opengl vendor string"
|
|
||||||
if mesa_info
|
|
||||||
gpus << gpu
|
|
||||||
gpu = {}
|
|
||||||
|
|
||||||
break
|
|
||||||
end
|
|
||||||
when /extended renderer info \(GLX_MESA_query_renderer\)/i
|
|
||||||
# Joy and happiness
|
|
||||||
mesa_info = true
|
|
||||||
when /Memory info \(GL_NVX_gpu_memory_info\)/i
|
|
||||||
# Happiness and joy
|
|
||||||
gpu_memory_info = true
|
|
||||||
when "vendor", "opengl vendor string"
|
|
||||||
gpu["manufacturer"] = value
|
|
||||||
when "device", "opengl renderer string"
|
|
||||||
gpu["model"] = value
|
|
||||||
when "version"
|
|
||||||
gpu["driver_version"] = value
|
|
||||||
when "video memory", "dedicated video memory"
|
|
||||||
gpu["vram"] = value.gsub(/[\D]+/, "")
|
|
||||||
when "opengl version string"
|
|
||||||
gpus << gpu
|
|
||||||
gpu = {}
|
|
||||||
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
gpus
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class W3DHub
|
|||||||
background_image_path = Cache.package_path(game.category, game.id, "background.png", "")
|
background_image_path = Cache.package_path(game.category, game.id, "background.png", "")
|
||||||
if File.exist?(background_image_path)
|
if File.exist?(background_image_path)
|
||||||
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path)
|
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.background_image = get_image(background_image_path)
|
||||||
|
States::Interface.instance&.instance_variable_get(:"@interface_container")&.style&.default[:background_image] = get_image(background_image_path)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Game Stuff
|
# Game Stuff
|
||||||
|
|||||||
@@ -196,13 +196,13 @@ class W3DHub
|
|||||||
|
|
||||||
def ping_icon(server)
|
def ping_icon(server)
|
||||||
case server.ping
|
case server.ping
|
||||||
when 0..150
|
when 0..50
|
||||||
@ping_icons[:good]
|
@ping_icons[:good]
|
||||||
when 151..200
|
when 51..150
|
||||||
@ping_icons[:fair]
|
@ping_icons[:fair]
|
||||||
when 201..1_000
|
when 151..200
|
||||||
@ping_icons[:poor]
|
@ping_icons[:poor]
|
||||||
when 1_001..5_000
|
when 201..1_000
|
||||||
@ping_icons[:bad]
|
@ping_icons[:bad]
|
||||||
else
|
else
|
||||||
@ping_icons[:unknown]
|
@ping_icons[:unknown]
|
||||||
@@ -271,8 +271,9 @@ class W3DHub
|
|||||||
def stylize_selected_server(server_container)
|
def stylize_selected_server(server_container)
|
||||||
server_container.style.background = @selected_color
|
server_container.style.background = @selected_color
|
||||||
|
|
||||||
server_container.style.hover.background = @selected_color
|
server_container.style.default[:background] = @selected_color
|
||||||
server_container.style.active.background = @selected_color
|
server_container.style.hover[:background] = @selected_color
|
||||||
|
server_container.style.active[:background] = @selected_color
|
||||||
end
|
end
|
||||||
|
|
||||||
def reorder_server_list
|
def reorder_server_list
|
||||||
@@ -283,11 +284,11 @@ class W3DHub
|
|||||||
end.reverse!.each_with_index do |child, i|
|
end.reverse!.each_with_index do |child, i|
|
||||||
next if @selected_server_container && child == @selected_server_container
|
next if @selected_server_container && child == @selected_server_container
|
||||||
|
|
||||||
child.style.hover.background = 0xaa_555566
|
child.style.hover[:background] = 0xaa_555566
|
||||||
child.style.active.background = 0xaa_555588
|
child.style.hover[:active] = 0xaa_555588
|
||||||
|
|
||||||
child.style.background = 0xaa_333333 if i.even?
|
child.style.default[:background] = 0xaa_333333 if i.even?
|
||||||
child.style.background = 0x00_000000 if i.odd?
|
child.style.default[:background] = 0x00_000000 if i.odd?
|
||||||
end
|
end
|
||||||
|
|
||||||
@server_list_container.recalculate
|
@server_list_container.recalculate
|
||||||
|
|||||||
166
lib/ping.rb
Normal file
166
lib/ping.rb
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
require "async"
|
||||||
|
require "socket"
|
||||||
|
require "securerandom"
|
||||||
|
|
||||||
|
class W3DHub
|
||||||
|
class Ping
|
||||||
|
ICMPHeader = Data.define(:type, :code, :checksum, :_ping_id, :_sequence_id, :data)
|
||||||
|
EchoRequest = Struct.new(:ping_id, :sequence_id, :data, :time, :timed_out)
|
||||||
|
|
||||||
|
ICMP_ECHOREPLY = 0
|
||||||
|
ICMP_ECHO = 8
|
||||||
|
ICMP_SUBCODE = 0
|
||||||
|
|
||||||
|
BIT_PACKER = "C2 n3 A*".freeze
|
||||||
|
MINIMUM_INTERVAL = 250 # ms # intervals below 200ms are considered rude and may be dropped due to flooding.
|
||||||
|
ECHO_REQUEST_HISTORY = 30 # 100 # keep the last n requests
|
||||||
|
|
||||||
|
attr_reader :address
|
||||||
|
|
||||||
|
def initialize(address:, count: 10, ttl: 120, interval: 1_000, data: nil)
|
||||||
|
@address = address
|
||||||
|
@count = count
|
||||||
|
@ttl = ttl
|
||||||
|
@interval = interval.to_i < MINIMUM_INTERVAL ? MINIMUM_INTERVAL : interval # ms
|
||||||
|
@data = data
|
||||||
|
|
||||||
|
# circular buffer
|
||||||
|
@echo_requests = Array.new(ECHO_REQUEST_HISTORY) { EchoRequest.new(-1, -1, "", nil, false) }
|
||||||
|
@echo_requests_index = 0
|
||||||
|
|
||||||
|
# NOTE: The PING_ID _might_ be overruled by the kernel and should not be used
|
||||||
|
# to check that any received echo replies are ours.
|
||||||
|
#
|
||||||
|
# Sequence ID and Data appear to be unmodified.
|
||||||
|
@ping_id = SecureRandom.hex.to_i(16) & 0xffff
|
||||||
|
@sequence_id = SecureRandom.hex.to_i(16) & 0xffff
|
||||||
|
|
||||||
|
addresses = Addrinfo.getaddrinfo(@address, nil, Socket::AF_INET, :DGRAM)
|
||||||
|
raise "NO ADDRESSES!" if addresses.empty?
|
||||||
|
|
||||||
|
@socket_address = addresses.sample.to_sockaddr
|
||||||
|
|
||||||
|
@socket = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_ICMP)
|
||||||
|
@socket.setsockopt(Socket::SOL_SOCKET, Socket::IP_TTL, @ttl)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Perform a checksum on the message. This is the sum of all the short
|
||||||
|
# words and it folds the high order bits into the low order bits.
|
||||||
|
def message_checksum(message)
|
||||||
|
length = message.length
|
||||||
|
num_short = length / 2
|
||||||
|
check = 0
|
||||||
|
|
||||||
|
message.unpack("n#{num_short}").each do |short|
|
||||||
|
check += short
|
||||||
|
end
|
||||||
|
|
||||||
|
check += message[length - 1, 1].unpack1("C") << 8 if (length % 2).positive?
|
||||||
|
|
||||||
|
check = (check >> 16) + (check & 0xffff)
|
||||||
|
~((check >> 16) + check) & 0xffff
|
||||||
|
end
|
||||||
|
|
||||||
|
def random_data
|
||||||
|
SecureRandom.hex
|
||||||
|
end
|
||||||
|
|
||||||
|
def monotonic_time
|
||||||
|
Process.clock_gettime(:CLOCK_MONOTONIC, :millisecond)
|
||||||
|
end
|
||||||
|
|
||||||
|
def verified?(message)
|
||||||
|
data = message.unpack(BIT_PACKER)
|
||||||
|
checksum = data[2]
|
||||||
|
|
||||||
|
# set checksum in message to 0
|
||||||
|
data[2] = 0
|
||||||
|
|
||||||
|
checksum == message_checksum(data.pack(BIT_PACKER))
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_complete?(request)
|
||||||
|
request.timed_out || !request.time.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
def packet_loss
|
||||||
|
completed_requests = @echo_requests.select { |r| request_complete?(r) }
|
||||||
|
failed_requests = completed_requests.select(&:timed_out)
|
||||||
|
|
||||||
|
# 0% packet loss 😎
|
||||||
|
return 0.0 if failed_requests.empty?
|
||||||
|
|
||||||
|
# 100% packet loss
|
||||||
|
return 1.0 if failed_requests.size == completed_requests.size
|
||||||
|
|
||||||
|
failed_requests.size / completed_requests.size.to_f
|
||||||
|
end
|
||||||
|
|
||||||
|
def average_ping
|
||||||
|
times = @echo_requests.select { |r| request_complete?(r) && !r.timed_out }.map(&:time)
|
||||||
|
|
||||||
|
return -1 unless times.size.positive?
|
||||||
|
|
||||||
|
times.sum.to_f / times.size
|
||||||
|
end
|
||||||
|
|
||||||
|
# returns true if any echo requests have completed (reply received or timed out) and packet loss is less than 30%
|
||||||
|
def okay?
|
||||||
|
completed_requests = @echo_requests.select { |r| request_complete?(r) }.size
|
||||||
|
|
||||||
|
completed_requests.positive? && packet_loss < 0.3
|
||||||
|
end
|
||||||
|
|
||||||
|
def ping(count = @count)
|
||||||
|
return if count <= 0
|
||||||
|
|
||||||
|
Async do |task|
|
||||||
|
@count.times do
|
||||||
|
task.Async do |subtask|
|
||||||
|
@sequence_id = (@sequence_id + 1) % 0xffff
|
||||||
|
data = @data || random_data
|
||||||
|
|
||||||
|
checksum = 0
|
||||||
|
message = [ICMP_ECHO, ICMP_SUBCODE, checksum, @ping_id, @sequence_id, data].pack(BIT_PACKER)
|
||||||
|
checksum = message_checksum(message)
|
||||||
|
message = [ICMP_ECHO, ICMP_SUBCODE, checksum, @ping_id, @sequence_id, data].pack(BIT_PACKER)
|
||||||
|
|
||||||
|
@socket.send(message, 0, @socket_address)
|
||||||
|
|
||||||
|
s = monotonic_time
|
||||||
|
request = @echo_requests[@echo_requests_index]
|
||||||
|
request.ping_id = @ping_id
|
||||||
|
request.sequence_id = @sequence_id
|
||||||
|
request.data = data
|
||||||
|
request.time = nil
|
||||||
|
request.timed_out = false
|
||||||
|
@echo_requests_index = (@echo_requests_index + 1) % ECHO_REQUEST_HISTORY
|
||||||
|
|
||||||
|
subtask.with_timeout(2) do
|
||||||
|
loop do
|
||||||
|
data, _addrinfo = @socket.recvfrom(1500)
|
||||||
|
|
||||||
|
# ignore corruption
|
||||||
|
next unless verified?(data)
|
||||||
|
|
||||||
|
header = ICMPHeader.new(*data.unpack(BIT_PACKER))
|
||||||
|
|
||||||
|
if header.type == ICMP_ECHOREPLY && header._sequence_id == request.sequence_id && header.data == request.data
|
||||||
|
duration = monotonic_time - s
|
||||||
|
request.time = duration
|
||||||
|
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue Async::TimeoutError
|
||||||
|
request.timed_out = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Don't send out pings in a flood, it's considered rude.
|
||||||
|
sleep @interval / 1000.0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
54
lib/ping_manager.rb
Normal file
54
lib/ping_manager.rb
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
class W3DHub
|
||||||
|
class PingManager
|
||||||
|
Container = Struct.new(:pinger, :last_ping_time_ms)
|
||||||
|
PING_INTERVAL = 60_000
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@containers = {}
|
||||||
|
@addresses = []
|
||||||
|
end
|
||||||
|
|
||||||
|
def monitor(task)
|
||||||
|
task.async do |subtask|
|
||||||
|
while BackgroundWorker.alive?
|
||||||
|
# activate new addresses
|
||||||
|
@addresses.each do |address|
|
||||||
|
@containers[address] ||= Container.new(Ping.new(address: address), -PING_INTERVAL * 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
# cleanup old addresses
|
||||||
|
@containers.each_key do |key|
|
||||||
|
@containers.delete(key) unless @addresses.find { |a| a == key }
|
||||||
|
end
|
||||||
|
|
||||||
|
# ping the pingers
|
||||||
|
@containers.each_value do |container|
|
||||||
|
next unless Gosu.milliseconds - container.last_ping_time_ms >= PING_INTERVAL
|
||||||
|
|
||||||
|
container.last_ping_time_ms = Gosu.milliseconds
|
||||||
|
|
||||||
|
subtask.async do
|
||||||
|
container.pinger.ping
|
||||||
|
# pp [container.pinger.address, container.pinger.average_ping]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep 0.001
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def add_address(address)
|
||||||
|
@addresses << address
|
||||||
|
@addresses.uniq!
|
||||||
|
end
|
||||||
|
|
||||||
|
def ping_for(address)
|
||||||
|
@containers[address]&.pinger&.average_ping&.round || -1
|
||||||
|
end
|
||||||
|
|
||||||
|
def remove_address(address)
|
||||||
|
@addresses.delete(address)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -78,7 +78,7 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def save_settings
|
def save_settings
|
||||||
File.write(SETTINGS_FILE_PATH, JSON.pretty_generate(@settings))
|
File.write(SETTINGS_FILE_PATH, @settings.to_json)
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_application_cache(json)
|
def save_application_cache(json)
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class W3DHub
|
|||||||
Api.on_thread(:_applications) do |applications|
|
Api.on_thread(:_applications) do |applications|
|
||||||
if applications
|
if applications
|
||||||
Store.applications = applications
|
Store.applications = applications
|
||||||
Store.settings.save_application_cache(JSON.pretty_generate(applications.data))
|
Store.settings.save_application_cache(applications.data.to_json)
|
||||||
@tasks[:applications][:complete] = true
|
@tasks[:applications][:complete] = true
|
||||||
else
|
else
|
||||||
# FIXME: Failed to retreive!
|
# FIXME: Failed to retreive!
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ class W3DHub
|
|||||||
@service_status = @options[:service_status]
|
@service_status = @options[:service_status]
|
||||||
@applications = @options[:applications]
|
@applications = @options[:applications]
|
||||||
|
|
||||||
@account_expire = Gosu.milliseconds
|
|
||||||
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
|
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
|
||||||
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # 5 minutes
|
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # 5 minutes
|
||||||
|
|
||||||
@@ -60,21 +59,25 @@ class W3DHub
|
|||||||
|
|
||||||
link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.servers").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::ServerBrowser)
|
page(W3DHub::Pages::ServerBrowser)
|
||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.community").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::Community)
|
page(W3DHub::Pages::Community)
|
||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.downloads").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::DownloadManager)
|
page(W3DHub::Pages::DownloadManager)
|
||||||
end
|
end
|
||||||
|
|
||||||
link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
link I18n.t(:"interface.settings").upcase, text_size: 34, font: BOLD_FONT, margin_left: 12 do
|
||||||
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
@interface_container.style.background_image = DEFAULT_BACKGROUND_IMAGE
|
||||||
|
@interface_container.style.default[:background_image] = DEFAULT_BACKGROUND_IMAGE
|
||||||
page(W3DHub::Pages::Settings)
|
page(W3DHub::Pages::Settings)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -129,13 +132,6 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
hide_application_taskbar
|
hide_application_taskbar
|
||||||
|
|
||||||
every(3_000) do
|
|
||||||
# NOTE: each method called, internally checks whether it should act.
|
|
||||||
refresh_account_token
|
|
||||||
refresh_applications
|
|
||||||
refresh_server_list
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def draw
|
def draw
|
||||||
@@ -150,6 +146,36 @@ class W3DHub
|
|||||||
@page&.update
|
@page&.update
|
||||||
|
|
||||||
update_interface_task_status(@interface_task_update_pending) if @interface_task_update_pending
|
update_interface_task_status(@interface_task_update_pending) if @interface_task_update_pending
|
||||||
|
|
||||||
|
if Gosu.milliseconds >= @applications_expire
|
||||||
|
@applications_expire = Gosu.milliseconds + 30_000
|
||||||
|
|
||||||
|
Api.on_thread(:_applications) do |applications|
|
||||||
|
if applications
|
||||||
|
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
|
||||||
|
|
||||||
|
Store.applications = applications
|
||||||
|
|
||||||
|
# TODO: Signal Games and ServerBrowser that applications have been updated
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if Gosu.milliseconds >= @server_list_expire
|
||||||
|
@server_list_expire = Gosu.milliseconds + 30_000
|
||||||
|
|
||||||
|
Api.on_thread(:server_list, 2) do |list|
|
||||||
|
if list
|
||||||
|
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # five minutes
|
||||||
|
|
||||||
|
Store.server_list_last_fetch = Gosu.milliseconds
|
||||||
|
|
||||||
|
Api::ServerListUpdater.instance.refresh_server_list(list)
|
||||||
|
|
||||||
|
BackgroundWorker.foreground_job(-> {}, ->(_) { States::Interface.instance&.update_server_browser(nil, :refresh_all) })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def button_down(id)
|
def button_down(id)
|
||||||
@@ -248,63 +274,6 @@ class W3DHub
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def refresh_account_token
|
|
||||||
return if Gosu.milliseconds < @account_expire
|
|
||||||
return unless account = Store.account
|
|
||||||
|
|
||||||
@account_expire = Gosu.milliseconds + 30_000
|
|
||||||
|
|
||||||
if (account.access_token_expiry - Time.now) / 60 <= 60 * 3 # Refresh if token expires within 3 hours
|
|
||||||
logger.info(LOG_TAG) { "Refreshing user login..." }
|
|
||||||
|
|
||||||
Api.on_thread(:refresh_user_login, account.refresh_token) do |refreshed_account|
|
|
||||||
if refreshed_account
|
|
||||||
Store.account = refreshed_account
|
|
||||||
|
|
||||||
Store.settings[:account][:data] = refreshed_account
|
|
||||||
else
|
|
||||||
Store.settings[:account] = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
Store.settings.save_settings
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def refresh_applications
|
|
||||||
return if Gosu.milliseconds < @applications_expire
|
|
||||||
|
|
||||||
@applications_expire = Gosu.milliseconds + 30_000
|
|
||||||
|
|
||||||
Api.on_thread(:_applications) do |applications|
|
|
||||||
if applications
|
|
||||||
@applications_expire = Gosu.milliseconds + APPLICATIONS_UPDATE_INTERVAL # ten minutes
|
|
||||||
|
|
||||||
Store.applications = applications
|
|
||||||
|
|
||||||
# TODO: Signal Games and ServerBrowser that applications have been updated
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def refresh_server_list
|
|
||||||
return if Gosu.milliseconds < @server_list_expire
|
|
||||||
|
|
||||||
@server_list_expire = Gosu.milliseconds + 30_000
|
|
||||||
|
|
||||||
Api.on_thread(:server_list, 2) do |list|
|
|
||||||
if list
|
|
||||||
@server_list_expire = Gosu.milliseconds + SERVER_LIST_UPDATE_INTERVAL # five minutes
|
|
||||||
|
|
||||||
Store.server_list_last_fetch = Gosu.milliseconds
|
|
||||||
|
|
||||||
Api::ServerListUpdater.instance.refresh_server_list(list)
|
|
||||||
|
|
||||||
BackgroundWorker.foreground_job(-> {}, ->(_) { States::Interface.instance&.update_server_browser(nil, :refresh_all) })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
class W3DHub
|
class W3DHub
|
||||||
DIR_NAME = "W3DHubAlt".freeze
|
DIR_NAME = "W3DHubAlt".freeze
|
||||||
VERSION = "0.9.2".freeze
|
VERSION = "0.9.0".freeze
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class W3DHub
|
|||||||
Store[:server_list] = []
|
Store[:server_list] = []
|
||||||
Store[:settings] = Settings.new
|
Store[:settings] = Settings.new
|
||||||
Store[:application_manager] = ApplicationManager.new
|
Store[:application_manager] = ApplicationManager.new
|
||||||
|
Store[:ping_manager] = PingManager.new
|
||||||
|
|
||||||
|
BackgroundWorker.parallel_job(-> { Async { |task| Store.ping_manager.monitor(task) } }, nil)
|
||||||
|
|
||||||
Store[:main_thread_queue] = []
|
Store[:main_thread_queue] = []
|
||||||
|
|
||||||
|
|||||||
@@ -228,34 +228,27 @@ class W3DHub
|
|||||||
@encrypted
|
@encrypted
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_file(path:, replace: false)
|
def add_file(path:)
|
||||||
return false unless File.exist?(path)
|
return false unless File.exist?(path)
|
||||||
return false if File.directory?(path)
|
return false if File.directory?(path)
|
||||||
|
|
||||||
entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
|
info = EntryInfoHeader.new(0, 0, File.size(path))
|
||||||
add_entry(entry: entry, replace: replace)
|
@entries << Entry.new(name: File.basename(path), path: path, info: info)
|
||||||
|
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_blob(path:, blob:, replace: false)
|
def add_blob(path:, blob:)
|
||||||
info = EntryInfoHeader.new(0, 0, blob.size)
|
info = EntryInfoHeader.new(0, 0, blob.size)
|
||||||
entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
@entries << Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
||||||
into.crc32 = @entries.last.calculate_crc32
|
into.crc32 = @entries.last.calculate_crc32
|
||||||
|
|
||||||
add_entry(entry: entry, replace: replace)
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_entry(entry:, replace: false)
|
def add_entry(entry:)
|
||||||
duplicate = @entries.find { |e| e.name.upcase == entry.name.upcase }
|
|
||||||
|
|
||||||
if duplicate
|
|
||||||
if replace
|
|
||||||
@entries.delete(duplicate)
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@entries << entry
|
@entries << entry
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ require_relative "lib/cache"
|
|||||||
require_relative "lib/settings"
|
require_relative "lib/settings"
|
||||||
require_relative "lib/ww_mix"
|
require_relative "lib/ww_mix"
|
||||||
require_relative "lib/ico"
|
require_relative "lib/ico"
|
||||||
|
require_relative "lib/ping"
|
||||||
|
require_relative "lib/ping_manager"
|
||||||
require_relative "lib/broadcast_server"
|
require_relative "lib/broadcast_server"
|
||||||
require_relative "lib/hardware_survey"
|
require_relative "lib/hardware_survey"
|
||||||
require_relative "lib/game_settings"
|
require_relative "lib/game_settings"
|
||||||
|
|||||||
Reference in New Issue
Block a user