mirror of
https://github.com/cyberarm/w3d_hub_linux_launcher.git
synced 2026-03-22 12:16:15 +00:00
Compare commits
3 Commits
d4e4697983
...
v0.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 68df923bea | |||
| ddbec8d72c | |||
| 70d4e0c40f |
@@ -1,35 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
> [!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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# 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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
> [!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
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
> [!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
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
> [!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
|
||||||
|
|||||||
@@ -255,14 +255,13 @@ class W3DHub
|
|||||||
!server.status.password &&
|
!server.status.password &&
|
||||||
server.status.player_count < server.status.max_players
|
server.status.player_count < server.status.max_players
|
||||||
end
|
end
|
||||||
server_options.sort_by! { |s| [s.status.player_count, s.ping] }.reverse!
|
|
||||||
|
|
||||||
# try to find server with lowest ping and matching version
|
# try to find server with lowest ping and matching version
|
||||||
found_server = server_options.find { |server| server.version == app_data[:installed_version] }
|
found_server = server_options.find { |server| server.version == app_data[:installed_version] }
|
||||||
# try to find server with lowest ping and undefined version
|
# try to find server with lowest ping and undefined version
|
||||||
found_server ||= server_options.find { |server| server.version == Api::ServerListServer::NO_OR_DEFAULT_VERSION }
|
found_server ||= server_options.find { |server| server.version == Api::ServerListServer::NO_OR_DEFAULT_VERSION }
|
||||||
|
|
||||||
found_server || nil
|
found_server ? found_server : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def play_now(app_id, channel)
|
def play_now(app_id, channel)
|
||||||
|
|||||||
@@ -746,6 +746,8 @@ 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)
|
||||||
|
|
||||||
@@ -765,20 +767,15 @@ 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 = patch_mix.entries.find { |e| e.name.casecmp?(file) }
|
patch_mix.entries.each do |entry|
|
||||||
target = target_mix.entries.find { |e| e.name.casecmp?(file) }
|
target_mix.add_entry(entry: entry, replace: true)
|
||||||
|
|
||||||
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)
|
temp_mix = W3DHub::WWMix.new(path: temp_mix_path, encrypted: target_mix.encrypted?)
|
||||||
target_mix.entries.each { |e| temp_mix.add_entry(entry: e) }
|
target_mix.entries.each { |e| temp_mix.add_entry(entry: e, replace: true) }
|
||||||
unless temp_mix.save
|
unless temp_mix.save
|
||||||
raise temp_mix.error_reason
|
raise temp_mix.error_reason
|
||||||
end
|
end
|
||||||
|
|||||||
70
lib/ping.rb
70
lib/ping.rb
@@ -1,70 +0,0 @@
|
|||||||
require "socket"
|
|
||||||
|
|
||||||
ICMPHeader = Data.define(:type, :code, :checksum, :_ping_id, :_sequence_id, :data)
|
|
||||||
|
|
||||||
ICMP_ECHOREPLY = 0 # Echo reply
|
|
||||||
ICMP_ECHO = 8 # Echo request
|
|
||||||
ICMP_SUBCODE = 0
|
|
||||||
|
|
||||||
# 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 checksum(msg)
|
|
||||||
length = msg.length
|
|
||||||
num_short = length / 2
|
|
||||||
check = 0
|
|
||||||
|
|
||||||
msg.unpack("n#{num_short}").each do |short|
|
|
||||||
check += short
|
|
||||||
end
|
|
||||||
|
|
||||||
if (length % 2).positive?
|
|
||||||
check += msg[length-1, 1].unpack1("C") << 8
|
|
||||||
end
|
|
||||||
|
|
||||||
check = (check >> 16) + (check & 0xffff)
|
|
||||||
~((check >> 16) + check) & 0xffff
|
|
||||||
end
|
|
||||||
|
|
||||||
ip_address = "127.0.0.1" # "example.com" # "timecrafters.org" #
|
|
||||||
@ping_id = 92_459_064_892 & 0xffff
|
|
||||||
@sequence = 1 % 65_536
|
|
||||||
data = ""
|
|
||||||
data_size = 56
|
|
||||||
data_size.times { |n| data << (n % 256).chr }
|
|
||||||
|
|
||||||
check = 0
|
|
||||||
packer = "C2 n3 A" << data_size.to_s
|
|
||||||
message = [ICMP_ECHO, ICMP_SUBCODE, check, @ping_id, @sequence, data].pack(packer)
|
|
||||||
check = checksum(message)
|
|
||||||
message = [ICMP_ECHO, ICMP_SUBCODE, check, @ping_id, @sequence, data].pack(packer)
|
|
||||||
message_header = ICMPHeader.new(*[ICMP_ECHO, ICMP_SUBCODE, check, @ping_id, @sequence, data])
|
|
||||||
socket = Socket.new(Socket::AF_INET, Socket::SOCK_DGRAM, Socket::IPPROTO_ICMP)
|
|
||||||
socket.send(message, 0, Socket.pack_sockaddr_in(0, ip_address))
|
|
||||||
s = Time.now
|
|
||||||
loop do
|
|
||||||
data, _addrinfo = socket.recvfrom(1500)
|
|
||||||
pp [message, data]
|
|
||||||
header = ICMPHeader.new(*data.unpack("C2 n3 A*"))
|
|
||||||
pp [message_header, header]
|
|
||||||
# reply_type = data[20, 2].unpack1("C2")
|
|
||||||
# pp reply_type
|
|
||||||
|
|
||||||
ping_id = header._ping_id
|
|
||||||
sequence = header._sequence_id
|
|
||||||
|
|
||||||
case header.type
|
|
||||||
when ICMP_ECHOREPLY
|
|
||||||
puts "ECHOREPLY"
|
|
||||||
end
|
|
||||||
|
|
||||||
pp [@ping_id, ping_id]
|
|
||||||
pp [@sequence, sequence]
|
|
||||||
|
|
||||||
if ping_id == @ping_id && sequence == @sequence && reply_type == ICMP_ECHOREPLY
|
|
||||||
puts "PING OKAY: #{Time.now - s}s"
|
|
||||||
break
|
|
||||||
end
|
|
||||||
|
|
||||||
break
|
|
||||||
end
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
class W3DHub
|
class W3DHub
|
||||||
DIR_NAME = "W3DHubAlt".freeze
|
DIR_NAME = "W3DHubAlt".freeze
|
||||||
VERSION = "0.9.0".freeze
|
VERSION = "0.9.1".freeze
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -228,27 +228,34 @@ class W3DHub
|
|||||||
@encrypted
|
@encrypted
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_file(path:)
|
def add_file(path:, replace: false)
|
||||||
return false unless File.exist?(path)
|
return false unless File.exist?(path)
|
||||||
return false if File.directory?(path)
|
return false if File.directory?(path)
|
||||||
|
|
||||||
info = EntryInfoHeader.new(0, 0, File.size(path))
|
entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
|
||||||
@entries << Entry.new(name: File.basename(path), path: path, info: info)
|
add_entry(entry: entry, replace: replace)
|
||||||
|
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_blob(path:, blob:)
|
def add_blob(path:, blob:, replace: false)
|
||||||
info = EntryInfoHeader.new(0, 0, blob.size)
|
info = EntryInfoHeader.new(0, 0, blob.size)
|
||||||
@entries << Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
|
||||||
into.crc32 = @entries.last.calculate_crc32
|
into.crc32 = @entries.last.calculate_crc32
|
||||||
|
|
||||||
true
|
add_entry(entry: entry, replace: replace)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_entry(entry:)
|
def add_entry(entry:, replace: false)
|
||||||
@entries << 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
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user