Merge branch 'diamante_develop' into develop

# Conflicts:
#	.gitmodules
This commit is contained in:
Louvenarde 2023-06-22 19:23:13 +02:00
commit 4b1aade899
33 changed files with 472 additions and 650 deletions

View File

@ -8,8 +8,7 @@ assignees: ''
---
_Do not open an issue here if you need help with modding or have a problem getting the client to run.
It is very likely your problem will be resolved by reading the [FAQ](https://xlabs.dev/iw4x_faq) carefully.
Ask in `iw4x-support` or `iw4x-modding` channels on the [Discord](https://discord.gg/sKeVmR3) server if you still have problems.
Ask in `iw4x-support` or `iw4x-modding` channels on the [Discord](https://discord.gg/tKTQJxcCkk) server if you still have problems.
If this does not apply, please continue by filling in the template below._
**What are you trying to do?**

View File

@ -7,7 +7,7 @@ assignees: ''
---
_Before opening a new feature request, please see [Issues](https://github.com/XLabsProject/iw4x-client/issues) and check that a similar issue does not already exist.
_Before opening a new feature request, please see [Issues](https://github.com/iw4x/iw4x-client/issues) and check that a similar issue does not already exist.
If this a new request, help us help you by filling in the template below._
**What problem will this solve?**

View File

@ -24,7 +24,7 @@ jobs:
- Release
steps:
- name: Check out files
uses: actions/checkout@v3.5.2
uses: actions/checkout@v3.5.3
with:
submodules: true
fetch-depth: 0
@ -55,15 +55,11 @@ jobs:
name: Deploy artifacts
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop')
if: github.event_name == 'push' && (github.ref == 'refs/heads/develop')
steps:
- name: Setup main environment
if: github.ref == 'refs/heads/master'
run: echo "XLABS_MASTER_PATH=${{ secrets.XLABS_MASTER_SSH_PATH }}" >> $GITHUB_ENV
- name: Setup develop environment
if: github.ref == 'refs/heads/develop'
run: echo "XLABS_MASTER_PATH=${{ secrets.XLABS_MASTER_SSH_PATH_DEV }}" >> $GITHUB_ENV
run: echo "DIAMANTE_MASTER_PATH=${{ secrets.DIAMANTE_MASTER_SSH_PATH }}" >> $GITHUB_ENV
- name: Download Release binaries
uses: actions/download-artifact@v3.0.2
@ -74,20 +70,11 @@ jobs:
- name: Install SSH key
uses: shimataro/ssh-key-action@v2.5.1
with:
key: ${{ secrets.XLABS_MASTER_SSH_PRIVATE_KEY }}
key: ${{ secrets.DIAMANTE_MASTER_SSH_PRIVATE_KEY }}
known_hosts: 'just-a-placeholder-so-we-dont-get-errors'
- name: Add known hosts
run: ssh-keyscan -H ${{ secrets.XLABS_MASTER_SSH_ADDRESS }} >> ~/.ssh/known_hosts
run: ssh-keyscan -H ${{ secrets.DIAMANTE_MASTER_SSH_ADDRESS }} >> ~/.ssh/known_hosts
# - name: Remove old data files
# run: ssh ${{ secrets.XLABS_MASTER_SSH_USER }}@${{ secrets.XLABS_MASTER_SSH_ADDRESS }} rm -rf ${{ env.XLABS_MASTER_PATH }}/iw4x/data/*
- name: Upload iw4x binary
run: rsync -avz iw4x.dll ${{ secrets.XLABS_MASTER_SSH_USER }}@${{ secrets.XLABS_MASTER_SSH_ADDRESS }}:${{ env.XLABS_MASTER_PATH }}/iw4x/
# - name: Upload data files
# run: rsync -avz ./data/ ${{ secrets.XLABS_MASTER_SSH_USER }}@${{ secrets.XLABS_MASTER_SSH_ADDRESS }}:${{ env.XLABS_MASTER_PATH }}/iw4x/data/
- name: Publish changes
run: ssh ${{ secrets.XLABS_MASTER_SSH_USER }}@${{ secrets.XLABS_MASTER_SSH_ADDRESS }} ${{ secrets.XLABS_MASTER_SSH_CHANGE_PUBLISH_COMMAND }}
- name: Upload iw4x-client binary
run: rsync -avz iw4x.dll ${{ secrets.DIAMANTE_MASTER_SSH_USER }}@${{ secrets.DIAMANTE_MASTER_SSH_ADDRESS }}:${{ env.DIAMANTE_MASTER_PATH }}/legacy/

View File

@ -1,17 +0,0 @@
name: Notify Discord
on:
push:
branches:
- "*"
issues:
jobs:
notify:
runs-on: ubuntu-latest
if: github.repository_owner == 'XLabsProject'
steps:
- name: Send notification to Discord
uses: Ilshidur/action-discord@master
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_CI_BOT_WEBHOOK }}

View File

@ -1,48 +0,0 @@
name: "Draft new release"
on:
workflow_dispatch:
inputs:
version:
description: "The version you want to release."
required: true
jobs:
draft-new-release:
name: "Draft a new release"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3.5.2
- name: Normalize version
id: normalize_version
run: |
version="${{ github.event.inputs.version }}"
version="r${version#v}"
echo "::set-output name=version::$version"
# Set up committer info and GPG key
- name: Import GPG key
id: import_gpg
uses: XLabsProject/ghaction-import-gpg@25d9d6ab99eb355c169c33c2306a72df85d9f516
with:
git-commit-gpgsign: true
git-committer-email: "${{ secrets.XLABS_CI_EMAIL }}"
git-committer-name: "${{ secrets.XLABS_CI_NAME }}"
# git-push-gpgsign: true
git-tag-gpgsign: true
git-user-signingkey: true
gpg-private-key: ${{ secrets.XLABS_CI_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.XLABS_CI_GPG_PASSWORD }}
- name: Create Pull Request
uses: repo-sync/pull-request@v2
with:
github_token: ${{ secrets.XLABS_CI_GITHUB_TOKEN }}
source_branch: "develop"
destination_branch: "master"
pr_allow_empty: true
pr_body: |
This Pull Request is for the release of IW4x ${{ steps.normalize_version.outputs.version }} and was [automatically created by a workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) triggered by @${{ github.actor }}.
pr_title: Release ${{ steps.normalize_version.outputs.version }}
pr_label: release

View File

@ -1,83 +0,0 @@
name: Release
on:
pull_request:
branches:
- "master"
types: [closed]
jobs:
merge:
runs-on: ubuntu-latest
name: Merge Release
steps:
- name: Check out files
if: github.event.pull_request.merged
uses: actions/checkout@v3.5.2
with:
submodules: false
lfs: false
# Set up committer info and GPG key
- name: Import GPG key
if: github.event.pull_request.merged
id: import_gpg
uses: XLabsProject/ghaction-import-gpg@25d9d6ab99eb355c169c33c2306a72df85d9f516
with:
git-commit-gpgsign: true
git-committer-email: "${{ secrets.XLABS_CI_EMAIL }}"
git-committer-name: "${{ secrets.XLABS_CI_NAME }}"
git-push-gpgsign: false
git-tag-gpgsign: true
git-user-signingkey: true
gpg-private-key: ${{ secrets.XLABS_CI_GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.XLABS_CI_GPG_PASSWORD }}
- name: Extract version from pull request
if: github.event.pull_request.merged
id: extract_version
run: |
title="${{ github.event.pull_request.title }}"
version="${title#Release }"
echo "::set-output name=version::$version"
- name: Create annotated tag
if: github.event.pull_request.merged
run: |
git tag -a -m "${{ github.event.pull_request.title }}" \
"${{ steps.extract_version.outputs.version }}" \
"${{ github.event.pull_request.merge_commit_sha }}"
git push origin --tags
- name: Create Pull Request
if: github.event.pull_request.merged
uses: repo-sync/pull-request@v2
with:
github_token: ${{ secrets.XLABS_CI_GITHUB_TOKEN }}
source_branch: "master"
destination_branch: "develop"
pr_allow_empty: true
pr_body: |
This Pull Request merges the release of IW4x ${{ steps.extract_version.outputs.version }} and was [automatically created by a workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) triggered by @${{ github.actor }}.
pr_title: Merge release ${{ steps.extract_version.outputs.version }}
pr_label: release
notify:
name: Notify Discord
runs-on: ubuntu-latest
if: |
github.repository_owner == 'XLabsProject' && (
(
github.event.pull_request.merged
) || (
github.event.push.ref == 'refs/heads/master' ||
github.event.push.ref == 'refs/heads/develop'
)
)
steps:
- name: Post CI status notification to Discord
uses: sarisia/actions-status-discord@v1.7.1
if: always()
with:
webhook: ${{ secrets.DISCORD_CI_BOT_WEBHOOK }}
title: "Build"

2
.gitmodules vendored
View File

@ -17,7 +17,7 @@
[submodule "deps/mongoose"]
path = deps/mongoose
url = https://github.com/cesanta/mongoose.git
branch = 7.9
branch = 7.10
[submodule "deps/protobuf"]
path = deps/protobuf
url = https://github.com/google/protobuf.git

View File

@ -1,10 +1,5 @@
![license](https://img.shields.io/github/license/IW4x/iw4x-client.svg)
![forks](https://img.shields.io/github/forks/IW4x/iw4x-client.svg)
![stars](https://img.shields.io/github/stars/IW4x/iw4x-client.svg)
![issues](https://img.shields.io/github/issues/IW4x/iw4x-client.svg)
[![build](https://github.com/XLabsProject/iw4x-client/workflows/Build/badge.svg)](https://github.com/XLabsProject/iw4x-client/actions)
[![discord](https://img.shields.io/endpoint?url=https://momo5502.com/iw4x/members-badge.php)](https://discord.gg/sKeVmR3)
[![patreon](https://img.shields.io/badge/patreon-support-blue.svg?logo=patreon)](https://www.patreon.com/xlabsproject)
![license](https://img.shields.io/github/license/iw4x/iw4x-client.svg)
[![build](https://github.com/iw4x/iw4x-client/workflows/Build/badge.svg)](https://github.com/iw4x/iw4x-client/actions)
# IW4x: Client
@ -37,10 +32,9 @@
| `-dump` | Write info of loaded assets to the raw folder as they are being loaded. |
| `-nointro` | Skip game's cinematic intro. |
| `-version` | Print IW4x build info on startup. |
| `-zonebuilder` | Start the interactive zonebuilder tool console instead of starting the game. |
| `-nosteam` | Disable friends feature and do not update Steam about the game's current status just like an invisible mode. |
| `-unprotect-dvars` | Allow the server to modify saved/archive dvars. |
| `-zonebuilder` | Start the interactive zonebuilder tool console instead of starting the game. |
## Disclaimer

2
deps/libtomcrypt vendored

@ -1 +1 @@
Subproject commit fae62af0ab16f469c2512ec04575dd60ca018657
Subproject commit 93f5348c47d3578091a4ee5b90f4add216b46d1b

2
deps/libtommath vendored

@ -1 +1 @@
Subproject commit 0df542cb70f621bbeec207be1949832fb1442479
Subproject commit c6a00c26ca2192c713a36227fdd84d126cdc95b9

2
deps/mongoose vendored

@ -1 +1 @@
Subproject commit 4236405b90e051310aadda818e21c811e404b4d8
Subproject commit b379816178abdcd59135aa32f990a4b3bbbfb54b

View File

@ -273,8 +273,7 @@ workspace "iw4x"
dependencies.imports()
-- Pre-build
prebuildcommands
{
prebuildcommands {
"pushd %{_MAIN_SCRIPT_DIR}",
"tools\\premake5 generate-buildinfo",
"popd",

View File

@ -371,7 +371,7 @@ namespace Components
MutedList.access([&](muteList& clients)
{
const nlohmann::json::array_t arr = list;
for (auto& entry : arr)
for (const auto& entry : arr)
{
if (entry.is_number_unsigned())
{

View File

@ -243,7 +243,6 @@ namespace Components
// Intercept time wrapping
Utils::Hook(0x62737D, TimeWrapStub, HOOK_CALL).install()->quick();
//Utils::Hook::Set<DWORD>(0x62735C, 50'000); // Time wrap after 50 seconds (for testing - i don't want to wait 3 weeks)
if (!ZoneBuilder::IsEnabled())
{

View File

@ -5,6 +5,7 @@
#include "Download.hpp"
#include "Events.hpp"
#include "MapRotation.hpp"
#include "Node.hpp"
#include "Party.hpp"
#include "ServerInfo.hpp"
@ -40,7 +41,7 @@ namespace Components
void Download::InitiateClientDownload(const std::string& mod, bool needPassword, bool map)
{
if (CLDownload.running) return;
if (CLDownload.running_) return;
Scheduler::Once([]
{
@ -61,26 +62,26 @@ namespace Components
return;
}
CLDownload.hashedPassword = Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(password), "");
CLDownload.hashedPassword_ = Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(password), "");
}
CLDownload.running = true;
CLDownload.isMap = map;
CLDownload.mod = mod;
CLDownload.terminateThread = false;
CLDownload.totalBytes = 0;
CLDownload.lastTimeStamp = 0;
CLDownload.downBytes = 0;
CLDownload.timeStampBytes = 0;
CLDownload.isPrivate = needPassword;
CLDownload.target = Party::Target();
CLDownload.thread = std::thread(ModDownloader, &CLDownload);
CLDownload.running_ = true;
CLDownload.isMap_ = map;
CLDownload.mod_ = mod;
CLDownload.terminateThread_ = false;
CLDownload.totalBytes_ = 0;
CLDownload.lastTimeStamp_ = 0;
CLDownload.downBytes_ = 0;
CLDownload.timeStampBytes_ = 0;
CLDownload.isPrivate_ = needPassword;
CLDownload.target_ = Party::Target();
CLDownload.thread_ = std::thread(ModDownloader, &CLDownload);
}
bool Download::ParseModList(ClientDownload* download, const std::string& list)
{
if (!download) return false;
download->files.clear();
download->files_.clear();
nlohmann::json listData;
try
@ -98,7 +99,7 @@ namespace Components
return false;
}
download->totalBytes = 0;
download->totalBytes_ = 0;
const nlohmann::json::array_t listDataArray = listData;
for (auto& file : listDataArray)
@ -118,8 +119,8 @@ namespace Components
if (!fileEntry.name.empty())
{
download->files.push_back(fileEntry);
download->totalBytes += fileEntry.size;
download->files_.push_back(fileEntry);
download->totalBytes_ += fileEntry.size;
}
}
catch (const nlohmann::json::exception& ex)
@ -134,12 +135,12 @@ namespace Components
bool Download::DownloadFile(ClientDownload* download, unsigned int index)
{
if (!download || download->files.size() <= index) return false;
if (!download || download->files_.size() <= index) return false;
auto file = download->files[index];
auto file = download->files_[index];
auto path = download->mod + "/" + file.name;
if (download->isMap)
auto path = download->mod_ + "/" + file.name;
if (download->isMap_)
{
path = "usermaps/" + path;
}
@ -149,16 +150,16 @@ namespace Components
auto data = Utils::IO::ReadFile(path);
if (data.size() == file.size && Utils::String::DumpHex(Utils::Cryptography::SHA256::Compute(data), "") == file.hash)
{
download->totalBytes += file.size;
download->totalBytes_ += file.size;
return true;
}
}
auto host = "http://" + download->target.getString();
auto host = "http://" + download->target_.getString();
auto fastHost = SV_wwwBaseUrl.get<std::string>();
if (Utils::String::StartsWith(fastHost, "https://"))
{
download->thread.detach();
download->thread_.detach();
download->clear();
Scheduler::Once([]
@ -197,8 +198,8 @@ namespace Components
}
else
{
url = host + "/file/" + (download->isMap ? "map/" : "") + file.name
+ (download->isPrivate ? ("?password=" + download->hashedPassword) : "");
url = host + "/file/" + (download->isMap_ ? "map/" : "") + file.name
+ (download->isPrivate_ ? ("?password=" + download->hashedPassword_) : "");
}
Logger::Print("Downloading from url {}\n", url);
@ -212,14 +213,14 @@ namespace Components
Utils::String::Replace(url, " ", "%20");
download->valid = true;
download->valid_ = true;
fDownload.downloading = true;
Utils::WebIO webIO;
webIO.setProgressCallback([&fDownload, &webIO](std::size_t bytes, std::size_t)
{
if(!fDownload.downloading || fDownload.download->terminateThread)
if(!fDownload.downloading || fDownload.download->terminateThread_)
{
webIO.cancelDownload();
return;
@ -234,14 +235,14 @@ namespace Components
fDownload.downloading = false;
download->valid = false;
download->valid_ = false;
if (fDownload.buffer.size() != file.size || Utils::Cryptography::SHA256::Compute(fDownload.buffer, true) != file.hash)
{
return false;
}
if (download->isMap) Utils::IO::CreateDir("usermaps/" + download->mod);
if (download->isMap_) Utils::IO::CreateDir("usermaps/" + download->mod_);
Utils::IO::WriteFile(path, fDownload.buffer);
return true;
@ -251,16 +252,16 @@ namespace Components
{
if (!download) download = &CLDownload;
const auto host = "http://" + download->target.getString();
const auto host = "http://" + download->target_.getString();
const auto listUrl = host + (download->isMap ? "/map" : "/list") + (download->isPrivate ? ("?password=" + download->hashedPassword) : "");
const auto listUrl = host + (download->isMap_ ? "/map" : "/list") + (download->isPrivate_ ? ("?password=" + download->hashedPassword_) : "");
const auto list = Utils::WebIO("IW4x", listUrl).setTimeout(5000)->get();
if (list.empty())
{
if (download->terminateThread) return;
if (download->terminateThread_) return;
download->thread.detach();
download->thread_.detach();
download->clear();
Scheduler::Once([]
@ -272,13 +273,13 @@ namespace Components
return;
}
if (download->terminateThread) return;
if (download->terminateThread_) return;
if (!ParseModList(download, list))
{
if (download->terminateThread) return;
if (download->terminateThread_) return;
download->thread.detach();
download->thread_.detach();
download->clear();
Scheduler::Once([]
@ -290,21 +291,21 @@ namespace Components
return;
}
if (download->terminateThread) return;
if (download->terminateThread_) return;
static std::string mod;
mod = download->mod;
mod = download->mod_;
for (std::size_t i = 0; i < download->files.size(); ++i)
for (std::size_t i = 0; i < download->files_.size(); ++i)
{
if (download->terminateThread) return;
if (download->terminateThread_) return;
if (!DownloadFile(download, i))
{
if (download->terminateThread) return;
if (download->terminateThread_) return;
mod = std::format("Failed to download file: {}!", download->files[i].name);
download->thread.detach();
mod = std::format("Failed to download file: {}!", download->files_[i].name);
download->thread_.detach();
download->clear();
Scheduler::Once([]
@ -320,12 +321,12 @@ namespace Components
}
}
if (download->terminateThread) return;
if (download->terminateThread_) return;
download->thread.detach();
download->thread_.detach();
download->clear();
if (download->isMap)
if (download->isMap_)
{
Scheduler::Once([]
{
@ -357,22 +358,22 @@ namespace Components
void Download::DownloadProgress(FileDownload* fDownload, std::size_t bytes)
{
fDownload->receivedBytes += bytes;
fDownload->download->downBytes += bytes;
fDownload->download->timeStampBytes += bytes;
fDownload->download->downBytes_ += bytes;
fDownload->download->timeStampBytes_ += bytes;
static volatile bool framePushed = false;
if (!framePushed)
{
double progress = 0;
if (fDownload->download->totalBytes)
if (fDownload->download->totalBytes_)
{
progress = (100.0 / fDownload->download->totalBytes) * fDownload->download->downBytes;
progress = (100.0 / fDownload->download->totalBytes_) * fDownload->download->downBytes_;
}
static std::uint32_t dlIndex, dlSize, dlProgress;
dlIndex = fDownload->index + 1;
dlSize = fDownload->download->files.size();
dlSize = fDownload->download->files_.size();
dlProgress = static_cast<std::uint32_t>(progress);
framePushed = true;
@ -383,18 +384,18 @@ namespace Components
}, Scheduler::Pipeline::MAIN);
}
auto delta = Game::Sys_Milliseconds() - fDownload->download->lastTimeStamp;
auto delta = Game::Sys_Milliseconds() - fDownload->download->lastTimeStamp_;
if (delta > 300)
{
const auto doFormat = fDownload->download->lastTimeStamp != 0;
fDownload->download->lastTimeStamp = Game::Sys_Milliseconds();
const auto doFormat = fDownload->download->lastTimeStamp_ != 0;
fDownload->download->lastTimeStamp_ = Game::Sys_Milliseconds();
const auto dataLeft = fDownload->download->totalBytes - fDownload->download->downBytes;
const auto dataLeft = fDownload->download->totalBytes_ - fDownload->download->downBytes_;
int timeLeft = 0;
if (fDownload->download->timeStampBytes)
if (fDownload->download->timeStampBytes_)
{
const double timeLeftD = ((1.0 * dataLeft) / fDownload->download->timeStampBytes) * delta;
const double timeLeftD = ((1.0 * dataLeft) / fDownload->download->timeStampBytes_) * delta;
timeLeft = static_cast<int>(timeLeftD);
}
@ -404,7 +405,7 @@ namespace Components
static int dlDelta, dlTimeLeft;
dlTimeLeft = timeLeft;
dlDelta = delta;
dlTsBytes = fDownload->download->timeStampBytes;
dlTsBytes = fDownload->download->timeStampBytes_;
Scheduler::Once([]
{
@ -413,7 +414,7 @@ namespace Components
}, Scheduler::Pipeline::MAIN);
}
fDownload->download->timeStampBytes = 0;
fDownload->download->timeStampBytes_ = 0;
}
}
@ -434,7 +435,7 @@ namespace Components
MongooseLogBuffer.push_back(c);
}
static std::string InfoHandler()
static std::optional<std::string> InfoHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
const auto status = ServerInfo::GetInfo();
const auto host = ServerInfo::GetHostInfo();
@ -480,10 +481,12 @@ namespace Components
}
info["players"] = players;
return nlohmann::json(info).dump();
std::string out = nlohmann::json(info).dump();
return { out };
}
static std::string ListHandler()
static std::optional<std::string> ListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
static nlohmann::json jsonList;
static std::filesystem::path fsGamePre;
@ -526,10 +529,12 @@ namespace Components
jsonList = fileList;
}
return jsonList.dump();
std::string out = jsonList.dump();
return { out };
}
static std::string MapHandler()
static std::optional<std::string> MapHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
static std::string mapNamePre;
static nlohmann::json jsonList;
@ -570,10 +575,12 @@ namespace Components
jsonList = fileList;
}
return jsonList.dump();
std::string out = jsonList.dump();
return { out };
}
static void FileHandler(mg_connection* c, const mg_http_message* hm)
static std::optional<std::string> FileHandler(mg_connection* c, const mg_http_message* hm)
{
std::string url(hm->uri.ptr, hm->uri.len);
@ -602,7 +609,7 @@ namespace Components
if ((!Maps::GetUserMap()->isValid() && !Party::IsInUserMapLobby()) || !isValidFile)
{
mg_http_reply(c, 403, "Content-Type: text/html\r\n", "%s", "403 - Forbidden");
return;
return {};
}
url = std::format("usermaps\\{}\\{}", mapName, url);
@ -612,7 +619,7 @@ namespace Components
if ((!url.ends_with(".iwd") && url != "mod.ff") || url.find("_svr_") != std::string::npos)
{
mg_http_reply(c, 403, "Content-Type: text/html\r\n", "%s", "403 - Forbidden");
return;
return {};
}
}
@ -633,10 +640,44 @@ namespace Components
mg_printf(c, "%s", "\r\n");
mg_send(c, file.data(), file.size());
}
return {};
}
static std::optional<std::string> ServerListHandler([[maybe_unused]] mg_connection* c, [[maybe_unused]] const mg_http_message* hm)
{
std::vector<std::string> servers;
const auto nodes = Node::GetNodes();
for (const auto& node : nodes)
{
const auto address = node.address.getString();
servers.emplace_back(address);
}
nlohmann::json jsonList = servers;
std::string out = jsonList.dump();
return { out };
}
static void EventHandler(mg_connection* c, const int ev, void* ev_data, [[maybe_unused]] void* fn_data)
{
using callback = std::function<std::optional<std::string>(mg_connection*, const mg_http_message*)>;
static const auto handlers = []() -> std::unordered_map<std::string, callback>
{
std::unordered_map<std::string, callback> f;
f["/file"] = FileHandler;
f["/info"] = InfoHandler;
f["/list"] = ListHandler;
f["/map"] = MapHandler;
f["/serverlist"] = ServerListHandler;
return f;
}();
if (ev != MG_EV_HTTP_MSG)
{
return;
@ -645,26 +686,24 @@ namespace Components
auto* hm = static_cast<mg_http_message*>(ev_data);
const std::string url(hm->uri.ptr, hm->uri.len);
if (url.starts_with("/info"))
auto handled = false;
for (auto i = handlers.begin(); i != handlers.end();)
{
const auto reply = InfoHandler();
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.data());
}
else if (url.starts_with("/list"))
if (url.starts_with(i->first))
{
const auto reply = ListHandler();
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.data());
}
else if (url.starts_with("/map"))
if (const auto reply = i->second(c, hm))
{
const auto reply = MapHandler();
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.data());
mg_http_reply(c, 200, "Content-Type: application/json\r\n", "%s", reply.value().data());
}
else if (url.starts_with("/file"))
{
FileHandler(c, hm);
handled = true;
break;
}
else
++i;
}
if (!handled)
{
mg_http_serve_opts opts = { .root_dir = "iw4x/html" }; // Serve local dir
mg_http_serve_dir(c, hm, &opts);

View File

@ -24,24 +24,24 @@ namespace Components
class ClientDownload
{
public:
ClientDownload(bool _isMap = false) : running(false), valid(false), terminateThread(false), isMap(_isMap), totalBytes(0), downBytes(0), lastTimeStamp(0), timeStampBytes(0) {}
ClientDownload(bool isMap = false) : running_(false), valid_(false), terminateThread_(false), isMap_(isMap), totalBytes_(0), downBytes_(0), lastTimeStamp_(0), timeStampBytes_(0) {}
~ClientDownload() { this->clear(); }
bool running;
bool valid;
bool terminateThread;
bool isMap;
bool isPrivate;
Network::Address target;
std::string hashedPassword;
std::string mod;
std::thread thread;
bool running_;
bool valid_;
bool terminateThread_;
bool isMap_;
bool isPrivate_;
Network::Address target_;
std::string hashedPassword_;
std::string mod_;
std::thread thread_;
std::size_t totalBytes;
std::size_t downBytes;
std::size_t totalBytes_;
std::size_t downBytes_;
int lastTimeStamp;
std::size_t timeStampBytes;
int lastTimeStamp_;
std::size_t timeStampBytes_;
class File
{
@ -51,24 +51,24 @@ namespace Components
std::size_t size;
};
std::vector<File> files;
std::vector<File> files_;
void clear()
{
this->terminateThread = true;
this->terminateThread_ = true;
if (this->thread.joinable())
if (this->thread_.joinable())
{
this->thread.join();
this->thread_.join();
}
this->running = false;
this->mod.clear();
this->files.clear();
this->running_ = false;
this->mod_.clear();
this->files_.clear();
if (this->valid)
if (this->valid_)
{
this->valid = false;
this->valid_ = false;
}
}
};

View File

@ -767,16 +767,7 @@ namespace Components
UIScript::Add("downloadDLC", []([[maybe_unused]] const UIScript::Token& token, [[maybe_unused]] const Game::uiInfo_s* info)
{
int dlc = token.get<int>();
for (const auto& pack : Maps::DlcPacks)
{
if (pack.index == dlc)
{
ShellExecuteW(0, 0, L"https://xlabs.dev/support_iw4x_client.html", 0, 0, SW_SHOW);
return;
}
}
const auto dlc = token.get<int>();
Game::ShowMessageBox(Utils::String::VA("DLC %d does not exist!", dlc), "ERROR");
});

View File

@ -14,7 +14,6 @@ namespace Components
static std::thread Thread;
static bool Terminate;
static bool DownloadUpdater();
static const char* GetNewsText();
};

View File

@ -15,6 +15,8 @@ namespace Components
bool Node::WasIngame = false;
const Game::dvar_t* Node::net_natFix;
bool Node::Entry::isValid() const
{
return (this->lastResponse.has_value() && !this->lastResponse->elapsed(NODE_HALFLIFE * 2));
@ -48,7 +50,7 @@ namespace Components
this->lastRequest->update();
Session::Send(this->address, "nodeListRequest");
Node::SendList(this->address);
SendList(this->address);
#ifdef NODE_SYSTEM_DEBUG
Logger::Debug("Sent request to {}", this->address.getString());
#endif
@ -56,7 +58,6 @@ namespace Components
void Node::Entry::reset()
{
// this->lastResponse.reset(); // This would invalidate the node, but maybe we don't want that?
this->lastRequest.reset();
}
@ -69,28 +70,54 @@ namespace Components
for (auto i = 0; i < list.nodes_size(); ++i)
{
const std::string& addr = list.nodes(i);
const auto& addr = list.nodes(i);
if (addr.size() == sizeof(sockaddr))
{
Node::Add(reinterpret_cast<sockaddr*>(const_cast<char*>(addr.data())));
Add(reinterpret_cast<sockaddr*>(const_cast<char*>(addr.data())));
}
}
}
void Node::LoadNodes()
{
Proto::Node::List list;
std::string nodes = Utils::IO::ReadFile("players/nodes.dat");
if (nodes.empty() || !list.ParseFromString(Utils::Compression::ZLib::Decompress(nodes))) return;
for (int i = 0; i < list.nodes_size(); ++i)
std::string data;
if (!Utils::IO::ReadFile("players/nodes.json", &data) || data.empty())
{
const std::string& addr = list.nodes(i);
return;
}
if (addr.size() == sizeof(sockaddr))
nlohmann::json nodes;
try
{
Node::Add(reinterpret_cast<sockaddr*>(const_cast<char*>(addr.data())));
nodes = nlohmann::json::parse(data);
}
catch (const std::exception& ex)
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "JSON Parse Error: {}\n", ex.what());
return;
}
if (!nodes.contains("nodes"))
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "nodes.json contains invalid data\n");
return;
}
const auto& list = nodes["nodes"];
if (!list.is_array())
{
return;
}
const nlohmann::json::array_t arr = list;
Logger::Print("Parsing {} nodes from nodes.json\n", arr.size());
for (const auto& entry : arr)
{
if (entry.is_string())
{
Network::Address address(entry.get<std::string>());
Add(address);
}
}
}
@ -99,29 +126,32 @@ namespace Components
{
if (Dedicated::IsEnabled() && Dedicated::SVLanOnly.get<bool>()) return;
std::vector<std::string> nodes;
static Utils::Time::Interval interval;
if (!force && !interval.elapsed(1min)) return;
interval.update();
Proto::Node::List list;
Mutex.lock();
Node::Mutex.lock();
for (auto& node : Node::Nodes)
for (auto& node : Nodes)
{
if (node.isValid())
if (node.isValid() || force)
{
std::string* str = list.add_nodes();
sockaddr addr = node.address.getSockAddr();
str->append(reinterpret_cast<char*>(&addr), sizeof(addr));
const auto address = node.address.getString();
nodes.emplace_back(address);
}
}
Node::Mutex.unlock();
Utils::IO::WriteFile("players/nodes.dat", Utils::Compression::ZLib::Compress(list.SerializeAsString()));
Mutex.unlock();
nlohmann::json out;
out["nodes"] = nodes;
Utils::IO::WriteFile("players/nodes.json", out.dump());
}
void Node::Add(Network::Address address)
void Node::Add(const Network::Address& address)
{
#ifndef DEBUG
if (address.isLocal() || address.isSelf()) return;
@ -129,23 +159,23 @@ namespace Components
if (!address.isValid()) return;
std::lock_guard _(Node::Mutex);
for (auto& session : Node::Nodes)
std::lock_guard _(Mutex);
for (auto& session : Nodes)
{
if (session.address == address) return;
}
Node::Entry node;
Entry node;
node.address = address;
Node::Nodes.push_back(node);
Nodes.push_back(node);
}
std::vector<Node::Entry> Node::GetNodes()
{
std::lock_guard _(Node::Mutex);
std::lock_guard _(Mutex);
return Node::Nodes;
return Nodes;
}
void Node::RunFrame()
@ -165,34 +195,34 @@ namespace Components
if (WasIngame) // our last frame we were in-game and now we aren't so touch all nodes
{
for (auto i = Node::Nodes.begin(); i != Node::Nodes.end();++i)
for (auto& entry : Nodes)
{
// clearing the last request and response times makes the
// dispatcher think its a new node and will force a refresh
i->lastRequest.reset();
i->lastResponse.reset();
entry.lastRequest.reset();
entry.lastResponse.reset();
}
WasIngame = false;
}
static Utils::Time::Interval frameLimit;
int interval = static_cast<int>(1000.0f / Dvar::Var("net_serverFrames").get<int>());
const auto interval = 1000 / ServerList::NETServerFrames.get<int>();
if (!frameLimit.elapsed(std::chrono::milliseconds(interval))) return;
frameLimit.update();
std::lock_guard _(Node::Mutex);
Dvar::Var queryLimit("net_serverQueryLimit");
std::lock_guard _(Mutex);
int sentRequests = 0;
for (auto i = Node::Nodes.begin(); i != Node::Nodes.end();)
for (auto i = Nodes.begin(); i != Nodes.end();)
{
if (i->isDead())
{
i = Node::Nodes.erase(i);
i = Nodes.erase(i);
continue;
}
if (sentRequests < queryLimit.get<int>() && i->requiresRequest())
if (sentRequests < ServerList::NETServerQueryLimit.get<int>() && i->requiresRequest())
{
++sentRequests;
i->sendRequest();
@ -204,17 +234,16 @@ namespace Components
void Node::Synchronize()
{
std::lock_guard _(Node::Mutex);
for (auto& node : Node::Nodes)
std::lock_guard _(Mutex);
for (auto& node : Nodes)
{
//if (node.isValid()) // Comment out to simulate 'syncnodes' behaviour
{
node.reset();
}
}
}
void Node::HandleResponse(Network::Address address, const std::string& data)
void Node::HandleResponse(const Network::Address& address, const std::string& data)
{
Proto::Node::List list;
if (!list.ParseFromString(data)) return;
@ -223,7 +252,7 @@ namespace Components
Logger::Debug("Received response from {}", address.getString());
#endif
std::lock_guard _(Node::Mutex);
std::lock_guard _(Mutex);
for (int i = 0; i < list.nodes_size(); ++i)
{
@ -231,7 +260,7 @@ namespace Components
if (addr.size() == sizeof(sockaddr))
{
Node::Add(reinterpret_cast<sockaddr*>(const_cast<char*>(addr.data())));
Add(reinterpret_cast<sockaddr*>(const_cast<char*>(addr.data())));
}
}
@ -251,7 +280,7 @@ namespace Components
#endif
}
for (auto& node : Node::Nodes)
for (auto& node : Nodes)
{
if (address == node.address)
{
@ -263,39 +292,41 @@ namespace Components
}
}
Node::Entry entry;
Entry entry;
entry.address = address;
entry.data.protocol = list.protocol();
entry.lastResponse.emplace(Utils::Time::Point());
Node::Nodes.push_back(entry);
Nodes.push_back(entry);
}
}
void Node::SendList(const Network::Address& address)
{
std::lock_guard _(Node::Mutex);
std::lock_guard _(Mutex);
// need to keep the message size below 1404 bytes else recipient will just drop it
std::vector<std::string> nodeListReponseMessages;
for (std::size_t curNode = 0; curNode < Node::Nodes.size();)
for (std::size_t curNode = 0; curNode < Nodes.size();)
{
Proto::Node::List list;
list.set_isnode(Dedicated::IsEnabled());
list.set_protocol(PROTOCOL);
list.set_port(Node::GetPort());
list.set_port(GetPort());
for (std::size_t i = 0; i < NODE_MAX_NODES_TO_SEND;)
{
if (curNode >= Node::Nodes.size())
if (curNode >= Nodes.size())
{
break;
}
auto node = Node::Nodes.at(curNode++);
auto& node = Nodes.at(curNode++);
if (node.isValid())
{
std::string* str = list.add_nodes();
auto* str = list.add_nodes();
sockaddr addr = node.address.getSockAddr();
str->append(reinterpret_cast<char*>(&addr), sizeof(addr));
@ -320,65 +351,100 @@ namespace Components
}
}
unsigned short Node::GetPort()
std::uint16_t Node::GetPort()
{
if (Dvar::Var("net_natFix").get<bool>()) return 0;
if (net_natFix->current.enabled) return 0;
return Network::GetPort();
}
void Node::Migrate()
{
Proto::Node::List list;
std::string nodes;
if (!Utils::IO::ReadFile("players/nodes.dat", &nodes) || nodes.empty())
{
return;
}
if (!list.ParseFromString(Utils::Compression::ZLib::Decompress(nodes)))
{
return;
}
std::vector<std::string> data;
for (auto i = 0; i < list.nodes_size(); ++i)
{
const std::string& addr = list.nodes(i);
if (addr.size() == sizeof(sockaddr))
{
Network::Address address(reinterpret_cast<sockaddr*>(const_cast<char*>(addr.data())));
data.emplace_back(address.getString());
}
}
nlohmann::json out;
out["nodes"] = data;
if (!Utils::IO::FileExists("players/nodes.json"))
{
Utils::IO::WriteFile("players/nodes.json", out.dump());
}
Utils::IO::RemoveFile("players/nodes.dat");
}
Node::Node()
{
if (ZoneBuilder::IsEnabled()) return;
Dvar::Register<bool>("net_natFix", false, 0, "Fix node registration for certain firewalls/routers");
net_natFix = Game::Dvar_RegisterBool("net_natFix", false, 0, "Fix node registration for certain firewalls/routers");
Scheduler::Loop([]
{
Node::StoreNodes(false);
}, Scheduler::Pipeline::ASYNC);
StoreNodes(false);
}, Scheduler::Pipeline::ASYNC, 5min);
Scheduler::Loop(Node::RunFrame, Scheduler::Pipeline::MAIN);
Scheduler::Loop(RunFrame, Scheduler::Pipeline::MAIN);
Session::Handle("nodeListResponse", Node::HandleResponse);
Session::Handle("nodeListResponse", HandleResponse);
Session::Handle("nodeListRequest", [](const Network::Address& address, [[maybe_unused]] const std::string& data)
{
Node::SendList(address);
SendList(address);
});
// Load stored nodes
auto loadNodes = []
Scheduler::OnGameInitialized([]
{
Node::LoadNodePreset();
Node::LoadNodes();
};
Migrate();
LoadNodePreset();
LoadNodes();
}, Scheduler::Pipeline::MAIN);
Scheduler::OnGameInitialized(loadNodes, Scheduler::Pipeline::MAIN);
Command::Add("listnodes", [](const Command::Params*)
Command::Add("listNodes", [](const Command::Params*)
{
Logger::Print("Nodes: {}\n", Node::Nodes.size());
Logger::Print("Nodes: {}\n", Nodes.size());
std::lock_guard _(Node::Mutex);
for (auto& node : Node::Nodes)
std::lock_guard _(Mutex);
for (const auto& node : Nodes)
{
Logger::Print("{}\t({})\n", node.address.getString(), node.isValid() ? "Valid" : "Invalid");
}
});
Command::Add("addnode", [](const Command::Params* params)
Command::Add("addNode", [](const Command::Params* params)
{
if (params->size() < 2) return;
auto address = Network::Address{ params->get(1) };
if (address.isValid())
{
Node::Add(address);
Add(address);
}
});
}
Node::~Node()
void Node::preDestroy()
{
std::lock_guard _(Node::Mutex);
Node::StoreNodes(true);
Node::Nodes.clear();
std::lock_guard _(Mutex);
StoreNodes(true);
Nodes.clear();
}
}

View File

@ -1,6 +1,6 @@
#pragma once
#define NODE_HALFLIFE (3 * 60 * 1000) //3min
#define NODE_HALFLIFE (3 * 60 * 1000) // 3min
#define NODE_MAX_NODES_TO_SEND 64
#define NODE_SEND_RATE 500ms
@ -12,7 +12,7 @@ namespace Components
class Data
{
public:
uint64_t protocol;
std::uint64_t protocol;
};
class Entry
@ -34,9 +34,9 @@ namespace Components
};
Node();
~Node();
void preDestroy() override;
static void Add(Network::Address address);
static void Add(const Network::Address& address);
static std::vector<Entry> GetNodes();
static void RunFrame();
static void Synchronize();
@ -46,7 +46,9 @@ namespace Components
static std::vector<Entry> Nodes;
static bool WasIngame;
static void HandleResponse(Network::Address address, const std::string& data);
static const Game::dvar_t* net_natFix;
static void HandleResponse(const Network::Address& address, const std::string& data);
static void SendList(const Network::Address& address);
@ -54,6 +56,8 @@ namespace Components
static void LoadNodes();
static void StoreNodes(bool force);
static unsigned short GetPort();
static std::uint16_t GetPort();
static void Migrate();
};
}

View File

@ -245,10 +245,10 @@ namespace Components
}
auto workingDir = std::filesystem::current_path().string();
auto binary = FileSystem::GetAppdataPath() / "data" / "iw4x" / *Game::sys_exitCmdLine;
const std::string binary = *Game::sys_exitCmdLine;
SetEnvironmentVariableA("XLABS_MW2_INSTALL", workingDir.data());
Utils::Library::LaunchProcess(binary.string(), "-singleplayer", workingDir);
SetEnvironmentVariableA("MW2_INSTALL", workingDir.data());
Utils::Library::LaunchProcess(binary, "-singleplayer", workingDir);
}
__declspec(naked) void QuickPatch::SND_GetAliasOffset_Stub()
@ -320,7 +320,7 @@ namespace Components
Utils::Hook::Set<void(*)(Game::XAssetHeader, void*)>(0x51FCDD, QuickPatch::R_AddImageToList_Hk);
Utils::Hook::Set<const char*>(0x41DB8C, "iw4x-sp.exe");
Utils::Hook::Set<const char*>(0x41DB8C, "iw4-sp.exe");
Utils::Hook(0x4D6989, QuickPatch::Sys_SpawnQuitProcess_Hk, HOOK_CALL).install()->quick();
// Fix crash as nullptr goes unchecked

View File

@ -127,10 +127,10 @@ namespace Components
{
Utils::InfoString info;
info.set("admin", Dvar::Var("_Admin").get<const char*>());
info.set("website", Dvar::Var("_Website").get<const char*>());
info.set("email", Dvar::Var("_Email").get<const char*>());
info.set("location", Dvar::Var("_Location").get<const char*>());
info.set("admin", Dvar::Var("_Admin").get<std::string>());
info.set("website", Dvar::Var("_Website").get<std::string>());
info.set("email", Dvar::Var("_Email").get<std::string>());
info.set("location", Dvar::Var("_Location").get<std::string>());
return info;
}

View File

@ -54,6 +54,11 @@ namespace Components
static bool GetMasterServer(const char* ip, int port, Game::netadr_t& address);
static bool UseMasterServer;
static Dvar::Var UIServerSelected;
static Dvar::Var UIServerSelectedMap;
static Dvar::Var NETServerQueryLimit;
static Dvar::Var NETServerFrames;
private:
enum class Column : int
{
@ -150,11 +155,6 @@ namespace Components
static std::vector<unsigned int> VisibleList;
static Dvar::Var UIServerSelected;
static Dvar::Var UIServerSelectedMap;
static Dvar::Var NETServerQueryLimit;
static Dvar::Var NETServerFrames;
static bool IsServerListOpen();
};
}

View File

@ -149,159 +149,10 @@ namespace Components
{
if (Dedicated::IsEnabled()) return;
// Do not execute this when building zones
if (!ZoneBuilder::IsEnabled())
{
// Correctly upgrade stats
Utils::Hook(0x42F088, StructuredData::UpdateVersionOffsets, HOOK_CALL).install()->quick();
// 15 or more custom classes
Utils::Hook::Set<BYTE>(0x60A2FE, NUM_CUSTOM_CLASSES);
return;
}
AssetHandler::OnLoad([](Game::XAssetType type, Game::XAssetHeader asset, const std::string& filename, bool* /*restrict*/)
{
// Only intercept playerdatadef loading
if (type != Game::ASSET_TYPE_STRUCTURED_DATA_DEF || filename != "mp/playerdata.def") return;
// Store asset
Game::StructuredDataDefSet* data = asset.structuredDataDefSet;
if (!data) return;
if (data->defCount != 1)
{
Logger::Error(Game::ERR_FATAL, "PlayerDataDefSet contains more than 1 definition!");
return;
}
if (data->defs[0].version != 155)
{
Logger::Error(Game::ERR_FATAL, "Initial PlayerDataDef is not version 155, patching not possible!");
return;
}
std::unordered_map<int, std::vector<std::vector<std::string>>> patchDefinitions;
std::unordered_map<int, std::unordered_map<std::string, std::string>> otherPatchDefinitions;
// First check if all versions are present
for (int i = 156;; ++i)
{
// We're on DB thread (OnLoad) so use DB thread for FS
FileSystem::File definition(std::format("{}/{}.json", filename, i), Game::FsThread::FS_THREAD_DATABASE);
if (!definition.exists()) break;
std::vector<std::vector<std::string>> enumContainer;
std::unordered_map<std::string, std::string> otherPatches;
nlohmann::json defData;
try
{
defData = nlohmann::json::parse(definition.getBuffer());
}
catch (const nlohmann::json::parse_error& ex)
{
Logger::PrintError(Game::CON_CHANNEL_ERROR, "JSON Parse Error: {}\n", ex.what());
return;
}
if (!defData.is_object())
{
Logger::Error(Game::ERR_FATAL, "PlayerDataDef patch for version {} is invalid!", i);
return;
}
for (auto pType = 0; pType < StructuredData::PlayerDataType::COUNT; ++pType)
{
auto enumData = defData[StructuredData::EnumTranslation[pType]];
std::vector<std::string> entryData;
if (enumData.is_array())
{
for (const auto& rawEntry : enumData)
{
if (rawEntry.is_string())
{
entryData.push_back(rawEntry.get<std::string>());
}
}
}
enumContainer.push_back(entryData);
}
auto other = defData["other"];
if (other.is_object())
{
for (auto& item : other.items())
{
if (item.value().is_string())
{
otherPatches[item.key()] = item.value().get<std::string>();
}
}
}
patchDefinitions[i] = enumContainer;
otherPatchDefinitions[i] = otherPatches;
}
// Nothing to patch
if (patchDefinitions.empty()) return;
// Reallocate the definition
auto* newData = StructuredData::MemAllocator.allocateArray<Game::StructuredDataDef>(data->defCount + patchDefinitions.size());
std::memcpy(&newData[patchDefinitions.size()], data->defs, sizeof Game::StructuredDataDef * data->defCount);
// Prepare the buffers
for (unsigned int i = 0; i < patchDefinitions.size(); ++i)
{
std::memcpy(&newData[i], data->defs, sizeof Game::StructuredDataDef);
newData[i].version = (patchDefinitions.size() - i) + 155;
// Reallocate the enum array
auto* newEnums = StructuredData::MemAllocator.allocateArray<Game::StructuredDataEnum>(data->defs->enumCount);
std::memcpy(newEnums, data->defs->enums, sizeof Game::StructuredDataEnum * data->defs->enumCount);
newData[i].enums = newEnums;
}
// Apply new data
data->defs = newData;
data->defCount += patchDefinitions.size();
// Patch the definition
for (unsigned int i = 0; i < data->defCount; ++i)
{
// No need to patch version 155
if (newData[i].version == 155) continue;
if (patchDefinitions.contains(newData[i].version))
{
auto patchData = patchDefinitions[newData[i].version];
auto otherData = otherPatchDefinitions[newData[i].version];
// Invalid patch data
if (patchData.size() != StructuredData::PlayerDataType::COUNT)
{
Logger::Error(Game::ERR_FATAL, "PlayerDataDef patch for version {} wasn't parsed correctly!", newData[i].version);
continue;
}
// Apply the patch data
for (auto pType = 0; pType < StructuredData::PlayerDataType::COUNT; ++pType)
{
if (!patchData[pType].empty())
{
StructuredData::PatchPlayerDataEnum(&newData[i], static_cast<StructuredData::PlayerDataType>(pType), patchData[pType]);
}
}
StructuredData::PatchAdditionalData(&newData[i], otherData);
}
}
});
}
}

View File

@ -63,7 +63,7 @@ BEGIN
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "XLabsProject"
VALUE "CompanyName", "IW4x"
#ifdef _DEBUG
VALUE "FileDescription", "IW4 client modification (DEBUG)"
#else
@ -71,7 +71,7 @@ BEGIN
#endif
VALUE "FileVersion", REVISION_STR
VALUE "InternalName", "iw4x"
VALUE "LegalCopyright", "Copyright 2023 The XLabsProject Team. All rights reserved."
VALUE "LegalCopyright", "Copyright 2023 The IW4x Team. All rights reserved."
VALUE "OriginalFilename", "iw4x.dll"
VALUE "ProductName", "IW4x"
VALUE "ProductVersion", REVISION_STR

View File

@ -143,6 +143,12 @@ using namespace std::literals;
#define BASEGAME_NAME "iw4mp_ceg.exe"
#define CLIENT_CONFIG "iw4x_config.cfg"
#define XFILE_MAGIC_UNSIGNED 0x3030317566665749
#define XFILE_VERSION 276
#define XFILE_HEADER_IW4X 0x78345749 // 'IW4x'
#define XFILE_VERSION_IW4X 3
// Resource stuff
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS

View File

@ -23,11 +23,7 @@ namespace Steam
if (!idBits)
{
if (Components::ZoneBuilder::IsEnabled())
{
idBits = *reinterpret_cast<unsigned __int64*>(const_cast<char*>("DEDICATE"));
}
else if (Components::Singleton::IsFirstInstance() && !Components::Dedicated::IsEnabled()) // ECDSA guid
if (Components::Singleton::IsFirstInstance() && !Components::Dedicated::IsEnabled()) // ECDSA guid
{
idBits = Components::Auth::GetKeyHash();
}

View File

@ -132,7 +132,7 @@ namespace Steam
void Proxy::RunGame()
{
if (Steam::Enabled() && !Components::Dedicated::IsEnabled() && !Components::ZoneBuilder::IsEnabled())
if (Steam::Enabled() && !Components::Dedicated::IsEnabled())
{
SetEnvironmentVariableA("SteamAppId", ::Utils::String::VA("%lu", Proxy::AppId));
SetEnvironmentVariableA("SteamGameId", ::Utils::String::VA("%llu", Proxy::AppId & 0xFFFFFF));
@ -146,7 +146,7 @@ namespace Steam
void Proxy::SetMod(const std::string& mod)
{
if (!Proxy::ClientUser || !Proxy::SteamApps || !Steam::Enabled() || Components::Dedicated::IsEnabled() || Components::ZoneBuilder::IsEnabled()) return;
if (!Proxy::ClientUser || !Proxy::SteamApps || !Steam::Enabled() || Components::Dedicated::IsEnabled()) return;
if (!Proxy::SteamApps->BIsSubscribedApp(Proxy::AppId))
{
@ -382,7 +382,7 @@ namespace Steam
SetDllDirectoryA(directoy.data());
if (!Components::Dedicated::IsEnabled() && !Components::ZoneBuilder::IsEnabled())
if (!Components::Dedicated::IsEnabled())
{
Proxy::LaunchWatchGuard();

View File

@ -130,7 +130,7 @@ namespace Steam
}
else
{
Proxy::SetMod("IW4x: Modern Warfare 2");
Proxy::SetMod("IW4y: Modern Warfare 2");
Proxy::RunGame();
}

View File

@ -5,8 +5,8 @@ namespace Utils
{
const char* Cache::Urls[] =
{
"https://raw.githubusercontent.com/XLabsProject/iw4x-client",
"https://xlabs.dev",
"https://raw.githubusercontent.com/diamante0018/iw4x-client",
"https://alterware.dev",
};
std::string Cache::ValidUrl;

View File

@ -61,7 +61,7 @@ namespace Utils
{
if (!key.isValid()) return {};
std::uint8_t buffer[512];
std::uint8_t buffer[512]{};
unsigned long length = sizeof(buffer);
ltc_mp = ltm_desc;
@ -92,7 +92,6 @@ namespace Utils
Key key;
register_prng(&sprng_desc);
register_hash(&sha1_desc);
ltc_mp = ltm_desc;
@ -105,15 +104,18 @@ namespace Utils
{
if (!key.isValid()) return {};
std::uint8_t buffer[512];
std::uint8_t buffer[512]{};
unsigned long length = sizeof(buffer);
register_prng(&sprng_desc);
register_hash(&sha1_desc);
const auto hash = SHA512::Compute(message);
const ltc_hash_descriptor& hash_desc = sha512_desc;
const int hash_index = register_hash(&hash_desc);
ltc_mp = ltm_desc;
rsa_sign_hash(reinterpret_cast<const std::uint8_t*>(message.data()), message.size(), buffer, &length, NULL, find_prng("sprng"), find_hash("sha1"), 0, key.getKeyPtr());
rsa_sign_hash_ex(reinterpret_cast<const std::uint8_t*>(hash.data()), hash.size(),
buffer, &length, LTC_PKCS_1_V1_5, nullptr, 0, hash_index, 0, key.getKeyPtr());
return std::string{ reinterpret_cast<char*>(buffer), length };
}
@ -122,12 +124,17 @@ namespace Utils
{
if (!key.isValid()) return false;
register_hash(&sha1_desc);
const auto hash = SHA512::Compute(message);
const ltc_hash_descriptor& hash_desc = sha512_desc;
const int hash_index = register_hash(&hash_desc);
ltc_mp = ltm_desc;
int result = 0;
return (rsa_verify_hash(reinterpret_cast<const std::uint8_t*>(signature.data()), signature.size(), reinterpret_cast<const std::uint8_t*>(message.data()), message.size(), find_hash("sha1"), 0, &result, key.getKeyPtr()) == CRYPT_OK && result != 0);
auto result = 0;
return (rsa_verify_hash_ex(reinterpret_cast<const std::uint8_t*>(signature.data()), signature.size(),
reinterpret_cast<const std::uint8_t*>(hash.data()), hash.size(), LTC_PKCS_1_V1_5,
hash_index, 0, &result, key.getKeyPtr()) == CRYPT_OK && result != 0);
}
#pragma endregion
@ -145,9 +152,9 @@ namespace Utils
encData.resize(text.size());
symmetric_CBC cbc;
int des3 = find_cipher("3des");
const auto des3 = find_cipher("3des");
cbc_start(des3, reinterpret_cast<const std::uint8_t*>(iv.data()), reinterpret_cast<const std::uint8_t*>(key.data()), key.size(), 0, &cbc);
cbc_start(des3, reinterpret_cast<const std::uint8_t*>(iv.data()), reinterpret_cast<const std::uint8_t*>(key.data()), static_cast<int>(key.size()), 0, &cbc);
cbc_encrypt(reinterpret_cast<const std::uint8_t*>(text.data()), reinterpret_cast<uint8_t*>(encData.data()), text.size(), &cbc);
cbc_done(&cbc);
@ -160,9 +167,9 @@ namespace Utils
decData.resize(data.size());
symmetric_CBC cbc;
int des3 = find_cipher("3des");
const auto des3 = find_cipher("3des");
cbc_start(des3, reinterpret_cast<const std::uint8_t*>(iv.data()), reinterpret_cast<const std::uint8_t*>(key.data()), key.size(), 0, &cbc);
cbc_start(des3, reinterpret_cast<const std::uint8_t*>(iv.data()), reinterpret_cast<const std::uint8_t*>(key.data()), static_cast<int>(key.size()), 0, &cbc);
cbc_decrypt(reinterpret_cast<const std::uint8_t*>(data.data()), reinterpret_cast<std::uint8_t*>(decData.data()), data.size(), &cbc);
cbc_done(&cbc);
@ -269,20 +276,21 @@ namespace Utils
#pragma region JenkinsOneAtATime
unsigned int JenkinsOneAtATime::Compute(const std::string& data)
std::size_t JenkinsOneAtATime::Compute(const std::string& data)
{
return Compute(data.data(), data.size());
}
unsigned int JenkinsOneAtATime::Compute(const char* key, std::size_t len)
std::size_t JenkinsOneAtATime::Compute(const char* key, const std::size_t len)
{
unsigned int hash, i;
std::size_t hash, i;
for (hash = i = 0; i < len; ++i)
{
hash += key[i];
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);

View File

@ -9,16 +9,16 @@ namespace Utils
class Token
{
public:
Token() { this->tokenString.clear(); };
Token(const Token& obj) : tokenString(obj.tokenString) { };
Token(const std::string& token) : tokenString(token.begin(), token.end()) { };
Token(const std::basic_string<uint8_t>& token) : tokenString(token.begin(), token.end()) { };
Token() { this->tokenString.clear(); }
Token(const Token& obj) : tokenString(obj.tokenString) { }
Token(const std::string& token) : tokenString(token.begin(), token.end()) { }
Token(const std::basic_string<std::uint8_t>& token) : tokenString(token.begin(), token.end()) { }
Token& operator++ ()
{
if (this->tokenString.empty())
{
this->tokenString.append(reinterpret_cast<uint8_t*>(const_cast<char *>("\0")), 1);
this->tokenString.append(reinterpret_cast<std::uint8_t*>(const_cast<char *>("\0")), 1);
}
else
{
@ -30,8 +30,7 @@ namespace Utils
if (!i)
{
// Prepend here, as /dev/urandom says so ;) https://github.com/IW4x/iw4x-client-node/wikis/technical-information#incrementing-the-token
this->tokenString = std::basic_string<std::uint8_t>(reinterpret_cast<std::uint8_t*>(const_cast<char*>("\0")), 1) + this->tokenString;
this->tokenString = std::basic_string{ reinterpret_cast<std::uint8_t*>(const_cast<char*>("\0")), 1 } + this->tokenString;
break;
}
}
@ -69,27 +68,27 @@ namespace Utils
{
return false;
}
else if (this->toString().size() < token.toString().size())
if (this->toString().size() < token.toString().size())
{
return true;
}
else if (this->toString().size() > token.toString().size())
if (this->toString().size() > token.toString().size())
{
return false;
}
else
{
auto lStr = this->toString();
auto rStr = token.toString();
for (unsigned int i = 0; i < lStr.size(); ++i)
for (std::size_t i = 0; i < lStr.size(); ++i)
{
if (lStr[i] < rStr[i])
{
return true;
}
}
}
return false;
}
@ -109,17 +108,12 @@ namespace Utils
return !(*this < token);
}
std::string toString()
[[nodiscard]] std::string toString() const
{
return std::string(this->tokenString.begin(), this->tokenString.end());
return std::string{ this->tokenString.begin(), this->tokenString.end() };
}
std::string toString() const
{
return std::string(this->tokenString.begin(), this->tokenString.end());
}
std::basic_string<uint8_t> toUnsignedString()
[[nodiscard]] std::basic_string<std::uint8_t> toUnsignedString() const
{
return this->tokenString;
}
@ -137,7 +131,7 @@ namespace Utils
{
public:
static std::string GenerateChallenge();
static uint32_t GenerateInt();
static std::uint32_t GenerateInt();
static void Initialize();
private:
@ -153,45 +147,47 @@ namespace Utils
Key() : keyStorage(new ecc_key)
{
ZeroMemory(this->getKeyPtr(), sizeof(*this->getKeyPtr()));
};
Key(ecc_key* key) : Key() { if (key) std::memmove(this->getKeyPtr(), key, sizeof(*key)); };
Key(ecc_key key) : Key(&key) {};
}
Key(ecc_key* key) : Key() { if (key) std::memmove(this->getKeyPtr(), key, sizeof(*key)); }
Key(ecc_key key) : Key(&key) {}
~Key()
{
if (this->keyStorage.use_count() <= 1)
{
this->free();
}
};
bool isValid()
{
return (!Utils::Memory::IsSet(this->getKeyPtr(), 0, sizeof(*this->getKeyPtr())));
}
ecc_key* getKeyPtr()
[[nodiscard]] bool isValid()
{
return (!Memory::IsSet(this->getKeyPtr(), 0, sizeof(*this->getKeyPtr())));
}
[[nodiscard]] ecc_key* getKeyPtr()
{
return this->keyStorage.get();
}
std::string getPublicKey()
[[nodiscard]] std::string getPublicKey()
{
uint8_t buffer[512] = { 0 };
DWORD length = sizeof(buffer);
std::uint8_t buffer[512]{};
unsigned long length = sizeof(buffer);
if (ecc_ansi_x963_export(this->getKeyPtr(), buffer, &length) == CRYPT_OK)
{
return std::string(reinterpret_cast<char*>(buffer), length);
return std::string{ reinterpret_cast<char*>(buffer), length };
}
return "";
return std::string{};
}
void set(const std::string& pubKeyBuffer)
{
this->free();
if (ecc_ansi_x963_import(reinterpret_cast<const uint8_t*>(pubKeyBuffer.data()), pubKeyBuffer.size(), this->getKeyPtr()) != CRYPT_OK)
if (ecc_ansi_x963_import(reinterpret_cast<const std::uint8_t*>(pubKeyBuffer.data()), pubKeyBuffer.size(), this->getKeyPtr()) != CRYPT_OK)
{
ZeroMemory(this->getKeyPtr(), sizeof(*this->getKeyPtr()));
}
@ -201,23 +197,23 @@ namespace Utils
{
this->free();
if (ecc_import(reinterpret_cast<const uint8_t*>(key.data()), key.size(), this->getKeyPtr()) != CRYPT_OK)
if (ecc_import(reinterpret_cast<const std::uint8_t*>(key.data()), key.size(), this->getKeyPtr()) != CRYPT_OK)
{
ZeroMemory(this->getKeyPtr(), sizeof(*this->getKeyPtr()));
}
}
std::string serialize(int type = PK_PRIVATE)
[[nodiscard]] std::string serialize(int type = PK_PRIVATE)
{
uint8_t buffer[4096] = { 0 };
DWORD length = sizeof(buffer);
std::uint8_t buffer[4096]{};
unsigned long length = sizeof(buffer);
if (ecc_export(buffer, &length, type, this->getKeyPtr()) == CRYPT_OK)
{
return std::string(reinterpret_cast<char*>(buffer), length);
return std::string{ reinterpret_cast<char*>(buffer), length };
}
return "";
return std::string{};
}
void free()
@ -266,14 +262,50 @@ namespace Utils
}
}
rsa_key* getKeyPtr()
[[nodiscard]] bool isValid()
{
return (!Memory::IsSet(this->getKeyPtr(), 0, sizeof(*this->getKeyPtr())));
}
[[nodiscard]] rsa_key* getKeyPtr()
{
return this->keyStorage.get();
}
bool isValid()
[[nodiscard]] std::string getPublicKey()
{
return (!Memory::IsSet(this->getKeyPtr(), 0, sizeof(*this->getKeyPtr())));
std::uint8_t buffer[4096]{};
unsigned long length = sizeof(buffer);
if (rsa_export(buffer, &length, PK_PUBLIC, this->getKeyPtr()) == CRYPT_OK)
{
return std::string{ reinterpret_cast<char*>(buffer), length };
}
return std::string{};
}
void set(const std::string& keyBuffer)
{
this->free();
if (rsa_import(reinterpret_cast<const std::uint8_t*>(keyBuffer.data()), keyBuffer.size(), this->getKeyPtr()) != CRYPT_OK)
{
ZeroMemory(this->getKeyPtr(), sizeof(*this->getKeyPtr()));
}
}
[[nodiscard]] std::string serialize(int type = PK_PRIVATE)
{
std::uint8_t buffer[4096]{};
unsigned long length = sizeof(buffer);
if (rsa_export(buffer, &length, type, this->getKeyPtr()) == CRYPT_OK)
{
return std::string{ reinterpret_cast<char*>(buffer), length };
}
return std::string{};
}
void free()
@ -334,8 +366,8 @@ namespace Utils
class JenkinsOneAtATime
{
public:
static unsigned int Compute(const std::string& data);
static unsigned int Compute(const char* key, std::size_t len);
static std::size_t Compute(const std::string& data);
static std::size_t Compute(const char* key, std::size_t len);
};
}
}

View File

@ -112,15 +112,15 @@ namespace Utils
auto* exeBaseName = std::wcsrchr(binaryPath, L'\\');
exeBaseName[0] = L'\0';
// Make the game work without the xlabs launcher
// Make the game work without the AlterWare launcher
SetCurrentDirectoryW(binaryPath);
}
void SetEnvironment()
{
wchar_t* buffer{};
char* buffer{};
std::size_t size{};
if (_wdupenv_s(&buffer, &size, L"XLABS_MW2_INSTALL") != 0 || buffer == nullptr)
if (_dupenv_s(&buffer, &size, "MW2_INSTALL") != 0 || buffer == nullptr)
{
SetLegacyEnvironment();
return;
@ -128,8 +128,8 @@ namespace Utils
const auto _0 = gsl::finally([&] { std::free(buffer); });
SetCurrentDirectoryW(buffer);
SetDllDirectoryW(buffer);
SetCurrentDirectoryA(buffer);
SetDllDirectoryA(buffer);
}
HMODULE GetNTDLL()