Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
033227eb83 | ||
|
|
388dd99f60 | ||
|
|
986db8acb8 | ||
|
|
1b94cbdd8d | ||
|
|
b2e2106912 | ||
|
|
3858d482e9 | ||
|
|
5e42a54bbf | ||
|
|
7766d47bda | ||
|
|
31dbcfdb4f | ||
|
|
43e2b69d66 | ||
|
|
7a0e0d1285 | ||
|
|
36d797b391 | ||
|
|
4e0b6440d9 | ||
|
|
eec90dae91 | ||
|
|
4e1c256fbe |
@@ -11,10 +11,9 @@ Also known as "The Cycle: Frontier".
|
|||||||
- [2. Unpack `Prospect.Server.Api`](#2-unpack-prospectserverapi)
|
- [2. Unpack `Prospect.Server.Api`](#2-unpack-prospectserverapi)
|
||||||
- [3. Patch the `hosts` file](#3-patch-the-hosts-file)
|
- [3. Patch the `hosts` file](#3-patch-the-hosts-file)
|
||||||
- [4. Generate and import SSL certificate](#4-generate-and-import-ssl-certificate)
|
- [4. Generate and import SSL certificate](#4-generate-and-import-ssl-certificate)
|
||||||
- [5. Add `steam_appid` to the game](#5-add-steam_appid-to-the-game)
|
- [5. Extract `LoaderPack` to the game](#5-extract-loaderpack-to-the-game)
|
||||||
- [6. Copy the `Prospect.Client.Loader` to the game](#6-copy-the-prospectclientloader-to-the-game)
|
- [6. Run the server](#6-run-the-server)
|
||||||
- [7. Run the server](#7-run-the-server)
|
- [7. Run the game](#7-run-the-game)
|
||||||
- [8. Run the game](#8-run-the-game)
|
|
||||||
- [Troubleshooting and FAQ](#troubleshooting-and-faq)
|
- [Troubleshooting and FAQ](#troubleshooting-and-faq)
|
||||||
- [How to remove the certificate?](#how-to-remove-the-certificate)
|
- [How to remove the certificate?](#how-to-remove-the-certificate)
|
||||||
- [`generate_ssl.exe` is flagged as a virus](#generate_sslexe-is-flagged-as-a-virus)
|
- [`generate_ssl.exe` is flagged as a virus](#generate_sslexe-is-flagged-as-a-virus)
|
||||||
@@ -30,10 +29,7 @@ Also known as "The Cycle: Frontier".
|
|||||||
* [x] EULA acceptance
|
* [x] EULA acceptance
|
||||||
* [x] Tutorial
|
* [x] Tutorial
|
||||||
* [x] Single-player station (Season 2 and Season 3):
|
* [x] Single-player station (Season 2 and Season 3):
|
||||||
* [ ] Onboarding
|
* [x] Onboarding
|
||||||
* [x] Talk to Badum
|
|
||||||
* [x] Equip items
|
|
||||||
* [ ] Deploy to Fortuna
|
|
||||||
* [ ] Matchmaking and deployment
|
* [ ] Matchmaking and deployment
|
||||||
* [x] Solo
|
* [x] Solo
|
||||||
* [ ] Squad
|
* [ ] Squad
|
||||||
@@ -42,16 +38,15 @@ Also known as "The Cycle: Frontier".
|
|||||||
* [x] Inventory and loadout
|
* [x] Inventory and loadout
|
||||||
* [ ] Loadout presets (Season 3)
|
* [ ] Loadout presets (Season 3)
|
||||||
* [x] Quests
|
* [x] Quests
|
||||||
* [ ] Job boards
|
|
||||||
* [x] Faction progression
|
* [x] Faction progression
|
||||||
* [ ] Season pass
|
* [ ] Season pass
|
||||||
* [ ] Shops
|
* [ ] Aurum Shops
|
||||||
* [ ] Daily shop
|
* [ ] Daily shop
|
||||||
* [ ] Weekly shop
|
* [ ] Weekly shop
|
||||||
* [ ] Shop rotation
|
* [ ] Shop rotation
|
||||||
* [x] Daily login
|
* [x] Daily login
|
||||||
* [x] Character appearance and emotes
|
* [x] Character appearance and emotes
|
||||||
* [x] Shops
|
* [x] Item Shops
|
||||||
* [x] Crafting station
|
* [x] Crafting station
|
||||||
* [x] Quarters
|
* [x] Quarters
|
||||||
* [x] Player balance
|
* [x] Player balance
|
||||||
@@ -63,15 +58,16 @@ Also known as "The Cycle: Frontier".
|
|||||||
* [ ] Game mechanics
|
* [ ] Game mechanics
|
||||||
* [x] Can deploy through terminal
|
* [x] Can deploy through terminal
|
||||||
* [x] Can deploy with loadout
|
* [x] Can deploy with loadout
|
||||||
* [ ] Can evac
|
* [x] Can evac
|
||||||
* [ ] Can do quests
|
* [ ] Can do quests
|
||||||
* [ ] Can gain/lose loot
|
* [x] Delivery quests
|
||||||
* [ ] Can use Alien Forge.
|
* [ ] Kill quests
|
||||||
* [ ] Map content
|
* [x] Can gain/lose loot
|
||||||
|
* [x] Can use Alien Forge.
|
||||||
|
* [x] Map content
|
||||||
* [x] Bright Sands
|
* [x] Bright Sands
|
||||||
* [x] Crescent Falls
|
* [x] Crescent Falls
|
||||||
* [x] Tharis Island
|
* [x] Tharis Island
|
||||||
* [ ] Alien Forge
|
|
||||||
|
|
||||||
## Running locally
|
## Running locally
|
||||||
|
|
||||||
@@ -91,21 +87,17 @@ Before you start, you'll need the following software downloaded and installed:
|
|||||||
|
|
||||||
1. [MongoDB Community Edition](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-8.0.4-signed.msi).
|
1. [MongoDB Community Edition](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-8.0.4-signed.msi).
|
||||||
|
|
||||||
1. [Latest `Prospect.Server.Api`](https://github.com/deiteris/Prospect/releases) from the Releases section.
|
1. [`Prospect.Server.Api` and `LoaderPack`](https://github.com/deiteris/Prospect/releases) from the Releases section:
|
||||||
|
|
||||||
1. [Latest `Prospect.Client.Loader`](https://github.com/deiteris/Prospect/releases) from the Releases section.
|
- For Season 3 (the latest Steam game client), use Build 6.
|
||||||
|
|
||||||
1. [.NET Runtime 8.0](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.11) installed.
|
- For Season 2 game client, use the latest version.
|
||||||
|
|
||||||
1. [ASP.NET Core 8.0](https://aka.ms/dotnet-core-applaunch?framework=Microsoft.AspNetCore.App&framework_version=8.0.0&arch=x64&rid=win-x64&os=win10) installed.
|
|
||||||
|
|
||||||
1. The Cycle: Frontier game client:
|
1. The Cycle: Frontier game client:
|
||||||
|
|
||||||
- The latest version from [Steam](https://steamcommunity.com/app/868270).
|
- The latest version from [Steam](https://steamcommunity.com/app/868270).
|
||||||
|
|
||||||
- Season 2 client. See [download instructions below](#11-how-to-download-season-2-client-from-steamdb-using-steam-console).
|
- Season 2 client version `4623363103423775682` from SteamDB. See [download instructions below](#11-how-to-download-season-2-client-from-steamdb-using-steam-console).
|
||||||
|
|
||||||
- Any other version, even Season 1 works.
|
|
||||||
|
|
||||||
#### 1.1 How to download Season 2 client from SteamDB using Steam console
|
#### 1.1 How to download Season 2 client from SteamDB using Steam console
|
||||||
|
|
||||||
@@ -164,40 +156,24 @@ to successfully communicate with the local server. Do the following:
|
|||||||
|
|
||||||
1. Click **Finish**. A **Security Warning** popup may appear, make sure it specifies `2EA46.playfabapi.com` certification authority and click **Yes**.
|
1. Click **Finish**. A **Security Warning** popup may appear, make sure it specifies `2EA46.playfabapi.com` certification authority and click **Yes**.
|
||||||
|
|
||||||
### 5. Add `steam_appid` to the game
|
### 5. Extract `LoaderPack` to the game
|
||||||
|
|
||||||
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
|
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
|
||||||
|
|
||||||
1. Right-click in the folder > **New** > **Text Document**, and name it `steam_appid`.
|
1. Open the `LoaderPack` archive.
|
||||||
|
|
||||||
1. Open `steam_appid` and enter `480`.
|
1. Drag and drop the contents of the `LoaderPack` archive to the game.
|
||||||
|
|
||||||
1. Save and close the file.
|
|
||||||
|
|
||||||
### 6. Copy the `Prospect.Client.Loader` to the game
|
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> The current version of the loader will fix loot and mob spawning only for the following game client versions:
|
|
||||||
>
|
|
||||||
> - SteamDB Manifest `4623363103423775682`.
|
|
||||||
> - The latest Steam client version.
|
|
||||||
|
|
||||||
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
|
|
||||||
|
|
||||||
1. Copy the downloaded `Prospect.Client.Loader` file into the folder.
|
|
||||||
|
|
||||||
1. Create a shortcut for the `Prospect.Client.Loader` that you will use later to launch the game.
|
1. Create a shortcut for the `Prospect.Client.Loader` that you will use later to launch the game.
|
||||||
|
|
||||||
### 7. Run the server
|
### 6. Run the server
|
||||||
|
|
||||||
Now you are all set! Open the folder with `Prospect.Server.Api` and run `Prospect.Server.Api.exe`. It will open a console if it runs successfully.
|
Now you are all set! Open the folder with `Prospect.Server.Api` and run `Prospect.Server.Api.exe`. It will open a console if it runs successfully.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> Do not close the console when you run the game.
|
> Do not close the console when you run the game.
|
||||||
>
|
|
||||||
> If the console does not open, make sure you have [.NET Runtime 8.0](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.11) and [ASP.NET Core 8.0](https://aka.ms/dotnet-core-applaunch?framework=Microsoft.AspNetCore.App&framework_version=8.0.0&arch=x64&rid=win-x64&os=win10) installed.
|
|
||||||
|
|
||||||
### 8. Run the game
|
### 7. Run the game
|
||||||
|
|
||||||
Once the server is running, make sure that Steam is running and open The Cycle: Frontier using the shortcut you've created before.
|
Once the server is running, make sure that Steam is running and open The Cycle: Frontier using the shortcut you've created before.
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,9 @@
|
|||||||
<PropertyGroup Label="Globals">
|
<PropertyGroup Label="Globals">
|
||||||
<VCProjectVersion>17.0</VCProjectVersion>
|
<VCProjectVersion>17.0</VCProjectVersion>
|
||||||
<Keyword>Win32Proj</Keyword>
|
<Keyword>Win32Proj</Keyword>
|
||||||
<ProjectGuid>{8170a17f-e536-4fdb-bc01-78bf09a9a48b}</ProjectGuid>
|
<ProjectGuid>{04b1d963-2bcb-4887-9d6d-04cb3366a348}</ProjectGuid>
|
||||||
<RootNamespace>ProspectClientLoader</RootNamespace>
|
<RootNamespace>ProspectClientLoader</RootNamespace>
|
||||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||||
<ProjectName>Prospect.Client.Loader</ProjectName>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||||
|
|||||||
@@ -54,28 +54,354 @@ HANDLE OpenTargetProcess(DWORD processID) {
|
|||||||
return hProcess;
|
return hProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CheckTutorialActorsOffset(HANDLE hProcess, uintptr_t address) {
|
bool WriteBytes(HANDLE hProcess, uintptr_t address, BYTE* newValues, size_t size) {
|
||||||
|
SIZE_T bytesWritten;
|
||||||
|
DWORD oldProtect;
|
||||||
|
if (VirtualProtectEx(hProcess, (LPVOID)address, size, PAGE_EXECUTE_READWRITE, &oldProtect)) {
|
||||||
|
if (WriteProcessMemory(hProcess, (LPVOID)address, newValues, size, &bytesWritten)) {
|
||||||
|
printf("Memory Write Success at address 0x%p\n", (void*)address);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
printf("Failed to write memory. Error: %lu\n", GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Restore original protection
|
||||||
|
VirtualProtectEx(hProcess, (LPVOID)address, size, oldProtect, &oldProtect);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
printf("Failed to change memory protection. Error: %lu\n", GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckForceLevelStreamingOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
BYTE buffer[2];
|
BYTE buffer[2];
|
||||||
SIZE_T bytesRead;
|
SIZE_T bytesRead;
|
||||||
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
return buffer[0] == 0x32 && buffer[1] == 0xDB;
|
return buffer[0] == 0x32 && buffer[1] == 0xDB;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
DWORD error = GetLastError();
|
DWORD error = GetLastError();
|
||||||
if (error == 299) {
|
if (error == 299) {
|
||||||
printf("Incomplete memory read. Retrying...\n");
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
Sleep(1000);
|
Sleep(1000);
|
||||||
return CheckTutorialActorsOffset(hProcess, address);
|
return CheckForceLevelStreamingOffset(hProcess, address);
|
||||||
}
|
}
|
||||||
printf("Failed to read memory. Error: %lu\n", error);
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CheckGPStringOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
|
BYTE buffer[2];
|
||||||
|
SIZE_T bytesRead;
|
||||||
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
|
return buffer[0] == 0x5F && buffer[1] == 0x00;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == 299) {
|
||||||
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
|
Sleep(1000);
|
||||||
|
return CheckForceLevelStreamingOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckWeakPointerCheckJumpOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
|
BYTE buffer[2];
|
||||||
|
SIZE_T bytesRead;
|
||||||
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
|
return buffer[0] == 0xF8 && buffer[1] == 0xFE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == 299) {
|
||||||
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
|
Sleep(1000);
|
||||||
|
return CheckWeakPointerCheckJumpOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckWeakPointerCheckOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
|
BYTE buffer[3];
|
||||||
|
SIZE_T bytesRead;
|
||||||
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
|
return buffer[0] == 0x0F && buffer[1] == 0x1F && buffer[2] == 0x40;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == 299) {
|
||||||
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
|
Sleep(1000);
|
||||||
|
return CheckWeakPointerCheckOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchForceLevelStreaming(HANDLE hProcess, uintptr_t imageBase) {
|
||||||
|
// Season 3 Patch force disable level streaming: sub_3DA7520 -> loc_3DA75B6
|
||||||
|
uintptr_t address = imageBase + 0x3DA75B6;
|
||||||
|
bool found = CheckForceLevelStreamingOffset(hProcess, address);
|
||||||
|
if (!found) {
|
||||||
|
// Season 2 Patch force disable level streaming: sub_3D19900 -> loc_3D19996
|
||||||
|
address = imageBase + 0x3D19996;
|
||||||
|
found = CheckForceLevelStreamingOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to patch force level streaming!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
BYTE newValues[2] = { 0xB3, 0x01 };
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address = imageBase + 0x5541C08;
|
||||||
|
found = CheckGPStringOffset(hProcess, address);
|
||||||
|
if (!found) {
|
||||||
|
address = imageBase + 0x54E0558;
|
||||||
|
found = CheckGPStringOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to find _GP string!\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
BYTE newValues[6] = { 0x47, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address = imageBase + 0x16D7C24;
|
||||||
|
found = CheckWeakPointerCheckOffset(hProcess, address);
|
||||||
|
// TODO
|
||||||
|
//if (!found) {
|
||||||
|
// address = imageBase + 0x54E0558;
|
||||||
|
// found = CheckWeakPointerCheckOffset(hProcess, address);
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to find Weak Pointer Check!\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
BYTE newValues[28] = {
|
||||||
|
0x48, 0x8B, 0x06,
|
||||||
|
0x48, 0x8D, 0x4D, 0x7F,
|
||||||
|
0x48, 0x89, 0x45, 0x7F,
|
||||||
|
0xE8, 0xDC, 0xC7, 0xA7, 0x00,
|
||||||
|
0x48, 0x85, 0xC0,
|
||||||
|
0x0F, 0x84, 0xEE, 0x00, 0x00, 0x00,
|
||||||
|
0x90, 0x90, 0x90
|
||||||
|
};
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address = imageBase + 0x16D7D34;
|
||||||
|
found = CheckWeakPointerCheckJumpOffset(hProcess, address);
|
||||||
|
// TODO
|
||||||
|
//if (!found) {
|
||||||
|
// address = imageBase + 0x54E0558;
|
||||||
|
// found = CheckWeakPointerCheckJumpOffset(hProcess, address);
|
||||||
|
//}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to find Weak Pointer Check Jump!\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
BYTE newValues[1] = { 0xEC };
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckUpdateInventoryFunctionOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
|
BYTE buffer[2];
|
||||||
|
SIZE_T bytesRead;
|
||||||
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
|
return buffer[0] == 0x84 && buffer[1] == 0xC0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == 299) {
|
||||||
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
|
Sleep(1000);
|
||||||
|
return CheckUpdateInventoryFunctionOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchUpdateInventory(HANDLE hProcess, uintptr_t imageBase) {
|
||||||
|
uintptr_t address = imageBase + 0x18D5A11;
|
||||||
|
bool found = CheckUpdateInventoryFunctionOffset(hProcess, address);
|
||||||
|
if (!found) {
|
||||||
|
address = imageBase + 0x3D19B19;
|
||||||
|
found = CheckUpdateInventoryFunctionOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to patch inventory update function!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
BYTE newValues[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckSetupPlayerContractsOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
|
BYTE buffer[2];
|
||||||
|
SIZE_T bytesRead;
|
||||||
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
|
return buffer[0] == 0x48 && buffer[1] == 0x8D;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == 299) {
|
||||||
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
|
Sleep(1000);
|
||||||
|
return CheckSetupPlayerContractsOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CheckUpdatePlayerContractsOffset(HANDLE hProcess, uintptr_t address) {
|
||||||
|
BYTE buffer[2];
|
||||||
|
SIZE_T bytesRead;
|
||||||
|
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
|
||||||
|
return buffer[0] == 0x3B && buffer[1] == 0xC1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
DWORD error = GetLastError();
|
||||||
|
if (error == 299) {
|
||||||
|
printf("Incomplete memory read. Retrying...\n");
|
||||||
|
Sleep(1000);
|
||||||
|
return CheckUpdatePlayerContractsOffset(hProcess, address);
|
||||||
|
}
|
||||||
|
printf("Failed to read memory. Error: %lu\n", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchLoadPlayerContracts(HANDLE hProcess, uintptr_t imageBase) {
|
||||||
|
uintptr_t address = imageBase + 0x171E9EC;
|
||||||
|
bool found = CheckUpdatePlayerContractsOffset(hProcess, address);
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to update player contracts function!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
BYTE newValues[4] = { 0x90, 0x90, 0xEB, 0x21 };
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
address = imageBase + 0x1706127;
|
||||||
|
found = CheckSetupPlayerContractsOffset(hProcess, address);
|
||||||
|
if (!found) {
|
||||||
|
printf("Failed to find setup player contracts function!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
BYTE newValues[39] = {
|
||||||
|
0x45, 0x33, 0xC0, 0x49, 0x8B, 0xD5, 0x48, 0x8B, 0x89, 0xD8, 0x00, 0x00, 0x00, 0xE8, 0x47,
|
||||||
|
0xE2, 0x10, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
|
||||||
|
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xEB
|
||||||
|
};
|
||||||
|
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
|
||||||
|
if (!result) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Inject(HANDLE hProcess) {
|
||||||
|
const wchar_t* dllPath = L"UE4SS.dll";
|
||||||
|
// Allocate space in the target process for the DLL path
|
||||||
|
size_t pathSize = (wcslen(dllPath) + 1) * sizeof(wchar_t);
|
||||||
|
LPVOID remotePath = VirtualAllocEx(hProcess, NULL, pathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
|
if (!remotePath) {
|
||||||
|
wprintf(L"Failed to allocate memory in remote process. Error: %lu\n", GetLastError());
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the DLL path into the target process
|
||||||
|
if (!WriteProcessMemory(hProcess, remotePath, dllPath, pathSize, NULL)) {
|
||||||
|
wprintf(L"Failed to write memory. Error: %lu\n", GetLastError());
|
||||||
|
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the address of LoadLibraryW in kernel32.dll
|
||||||
|
LPVOID loadLibraryAddr = (LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
|
||||||
|
if (!loadLibraryAddr) {
|
||||||
|
wprintf(L"Failed to get LoadLibraryW address. Error: %lu\n", GetLastError());
|
||||||
|
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a remote thread to call LoadLibraryW with our DLL path
|
||||||
|
HANDLE hThread = CreateRemoteThread(
|
||||||
|
hProcess, NULL, 0,
|
||||||
|
(LPTHREAD_START_ROUTINE)loadLibraryAddr,
|
||||||
|
remotePath, 0, NULL);
|
||||||
|
|
||||||
|
if (!hThread) {
|
||||||
|
wprintf(L"Failed to create remote thread. Error: %lu\n", GetLastError());
|
||||||
|
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
|
||||||
|
CloseHandle(hProcess);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool Patch(const wchar_t* processName) {
|
bool Patch(const wchar_t* processName) {
|
||||||
DWORD processID;
|
DWORD processID;
|
||||||
uintptr_t imageBase, address;
|
uintptr_t imageBase;
|
||||||
BYTE newValues[2] = { 0xB3, 0x01 };
|
|
||||||
SIZE_T bytesWritten;
|
|
||||||
|
|
||||||
HANDLE hProcess = StartProcess(processName, &processID);
|
HANDLE hProcess = StartProcess(processName, &processID);
|
||||||
if (hProcess == NULL) {
|
if (hProcess == NULL) {
|
||||||
@@ -92,33 +418,13 @@ bool Patch(const wchar_t* processName) {
|
|||||||
}
|
}
|
||||||
printf("PE Image Base Address: 0x%p\n", (void*)imageBase);
|
printf("PE Image Base Address: 0x%p\n", (void*)imageBase);
|
||||||
|
|
||||||
// Season 3 Patch tutorial actors loader: sub_3DA7520 -> loc_3DA75B6
|
bool success = !PatchForceLevelStreaming(hProcess, imageBase) || !PatchUpdateInventory(hProcess, imageBase) || !PatchLoadPlayerContracts(hProcess, imageBase);
|
||||||
address = imageBase + 0x3DA75B6;
|
if (success) {
|
||||||
bool found = CheckTutorialActorsOffset(hProcess, address);
|
|
||||||
if (!found) {
|
|
||||||
// Season 2 Patch tutorial actors loader: sub_3D19900 -> loc_3D19996
|
|
||||||
address = imageBase + 0x3D19996;
|
|
||||||
found = CheckTutorialActorsOffset(hProcess, address);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
printf("Failed to find necessary memory chunk!");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
DWORD oldProtect;
|
if (!Inject(hProcess)) {
|
||||||
if (VirtualProtectEx(hProcess, (LPVOID)address, sizeof(newValues), PAGE_EXECUTE_READWRITE, &oldProtect)) {
|
return false;
|
||||||
if (WriteProcessMemory(hProcess, (LPVOID)address, newValues, sizeof(newValues), &bytesWritten)) {
|
|
||||||
printf("Memory Write Success: Wrote 0xB3 0x01 at address 0x%p\n", (void*)address);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
printf("Failed to write memory. Error: %lu\n", GetLastError());
|
|
||||||
}
|
|
||||||
// Restore original protection
|
|
||||||
VirtualProtectEx(hProcess, (LPVOID)address, sizeof(newValues), oldProtect, &oldProtect);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
printf("Failed to change memory protection. Error: %lu\n", GetLastError());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up
|
// Clean up
|
||||||
@@ -126,8 +432,42 @@ bool Patch(const wchar_t* processName) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WriteSteamAppIDFile() {
|
||||||
|
const char* filename = "steam_appid.txt";
|
||||||
|
FILE* fp;
|
||||||
|
|
||||||
|
if (fopen_s(&fp, filename, "r") == 0) {
|
||||||
|
// File exists, close the file
|
||||||
|
fclose(fp);
|
||||||
|
printf("File '%s' already exists.\n", filename);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// File does not exist, create and write to it
|
||||||
|
if (fopen_s(&fp, filename, "w") == 0) {
|
||||||
|
const char* content = "480";
|
||||||
|
size_t written = fwrite(content, sizeof(char), strlen(content), fp);
|
||||||
|
fclose(fp);
|
||||||
|
printf("File '%s' was created and written successfully.\n", filename);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
printf("Error creating the file '%s'.\n", filename);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
bool success = Patch(L"Prospect-Win64-Shipping.exe");
|
bool success = WriteSteamAppIDFile();
|
||||||
|
if (!success) {
|
||||||
|
// Wait for user input to exit
|
||||||
|
printf("An error occurred when writing steam_appid.txt! Press any key to exit...\n");
|
||||||
|
getchar();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
success = Patch(L"Prospect-Win64-Shipping.exe");
|
||||||
if (!success) {
|
if (!success) {
|
||||||
// Wait for user input to exit
|
// Wait for user input to exit
|
||||||
printf("An error occurred when patching the executable! Press any key to exit...\n");
|
printf("An error occurred when patching the executable! Press any key to exit...\n");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
||||||
|
|
||||||
namespace Prospect.Server.Api.Models.Data;
|
namespace Prospect.Server.Api.Models.Data;
|
||||||
|
|
||||||
@@ -11,5 +12,14 @@ public class PlayerBalance
|
|||||||
public int SoftCurrency { get; set; }
|
public int SoftCurrency { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("IN")]
|
[JsonPropertyName("IN")]
|
||||||
public int InsuranceTokens { get; set; }
|
public int InsuranceCurrency { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JobBoardsData
|
||||||
|
{
|
||||||
|
[JsonPropertyName("lastBoardRefreshTimeUtc")]
|
||||||
|
public FYTimestamp LastBoardRefreshTimeUtc { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("boards")]
|
||||||
|
public List<FYFactionContractsData> Boards { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Prospect.Server.Api.Services.Auth.Extensions;
|
using Prospect.Server.Api.Services.Auth.Extensions;
|
||||||
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
||||||
@@ -59,8 +59,7 @@ public class ActivateContract : ICloudScriptFunction<FYActivateContractRequest,
|
|||||||
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
|
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
|
||||||
|
|
||||||
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
|
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
|
||||||
var contract = contracts[request.ContractID];
|
if (!contracts.TryGetValue(request.ContractID, out var contract)) {
|
||||||
if (contract == null) {
|
|
||||||
return new FYActivateContractResult
|
return new FYActivateContractResult
|
||||||
{
|
{
|
||||||
UserID = userId,
|
UserID = userId,
|
||||||
@@ -71,6 +70,14 @@ public class ActivateContract : ICloudScriptFunction<FYActivateContractRequest,
|
|||||||
// TODO: Contract unlock criteria
|
// TODO: Contract unlock criteria
|
||||||
|
|
||||||
var progress = new int[contract.Objectives.Length];
|
var progress = new int[contract.Objectives.Length];
|
||||||
|
// TODO: Automatically set max progress of kill and visit objectives. To remove once kill and visit objectives are reported by the client.
|
||||||
|
for (var i = 0; i < contract.Objectives.Length; i++) {
|
||||||
|
var objective = contract.Objectives[i];
|
||||||
|
if (objective.Type != EYContractObjectiveType.OwnNumOfItem) {
|
||||||
|
progress[i] = objective.MaxProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
||||||
var contractData = new FYActiveContractPlayerData {
|
var contractData = new FYActiveContractPlayerData {
|
||||||
ContractID = request.ContractID,
|
ContractID = request.ContractID,
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly UserDataService _userDataService;
|
private readonly UserDataService _userDataService;
|
||||||
private readonly TitleDataService _titleDataService;
|
private readonly TitleDataService _titleDataService;
|
||||||
|
private readonly string[] difficulties = ["Easy", "Medium", "Hard"];
|
||||||
|
private readonly Random rnd = new Random();
|
||||||
|
|
||||||
public ClaimActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
|
public ClaimActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
|
||||||
{
|
{
|
||||||
@@ -66,11 +68,10 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
throw new CloudScriptException("CloudScript was not called within a http request");
|
throw new CloudScriptException("CloudScript was not called within a http request");
|
||||||
}
|
}
|
||||||
var userId = context.User.FindAuthUserId();
|
var userId = context.User.FindAuthUserId();
|
||||||
var titleData = _titleDataService.Find(new List<string>{"Contracts", "Blueprints"});
|
var titleData = _titleDataService.Find(new List<string>{ "Contracts", "Blueprints", "Jobs", "LevelData" });
|
||||||
|
|
||||||
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
|
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
|
||||||
var contract = contracts[request.ContractID];
|
if (!contracts.TryGetValue(request.ContractID, out var contract)) {
|
||||||
if (contract == null) {
|
|
||||||
return new FYClaimCompletedActiveContractRewardsResult
|
return new FYClaimCompletedActiveContractRewardsResult
|
||||||
{
|
{
|
||||||
UserID = userId,
|
UserID = userId,
|
||||||
@@ -82,16 +83,17 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
var factionKey = "FactionProgression" + contract.Faction;
|
var factionKey = "FactionProgression" + contract.Faction;
|
||||||
var userData = await _userDataService.FindAsync(
|
var userData = await _userDataService.FindAsync(
|
||||||
userId, userId,
|
userId, userId,
|
||||||
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey}
|
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey, "JobBoardsData" }
|
||||||
);
|
);
|
||||||
|
|
||||||
var factionProgression = JsonSerializer.Deserialize<int>(userData[factionKey].Value);
|
var factionProgression = JsonSerializer.Deserialize<int>(userData[factionKey].Value);
|
||||||
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
||||||
var contractsCompleted = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(userData["ContractsOneTimeCompleted"].Value);
|
var contractsCompleted = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(userData["ContractsOneTimeCompleted"].Value);
|
||||||
|
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value);
|
||||||
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
|
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
|
||||||
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
|
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
|
||||||
|
|
||||||
var targetContractIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == request.ContractID);
|
var targetContractIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == contract.Name);
|
||||||
if (targetContractIdx == -1) {
|
if (targetContractIdx == -1) {
|
||||||
return new FYClaimCompletedActiveContractRewardsResult
|
return new FYClaimCompletedActiveContractRewardsResult
|
||||||
{
|
{
|
||||||
@@ -106,6 +108,7 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
var targetContract = contractsActive.Contracts[targetContractIdx];
|
var targetContract = contractsActive.Contracts[targetContractIdx];
|
||||||
List<FYCustomItemInfo> itemsUpdatedOrRemoved = [];
|
List<FYCustomItemInfo> itemsUpdatedOrRemoved = [];
|
||||||
HashSet<string> deletedItemsIds = [];
|
HashSet<string> deletedItemsIds = [];
|
||||||
|
// TODO: Optimize
|
||||||
for (var i = 0; i < contract.Objectives.Length; i++) {
|
for (var i = 0; i < contract.Objectives.Length; i++) {
|
||||||
var objective = contract.Objectives[i];
|
var objective = contract.Objectives[i];
|
||||||
int remaining = objective.MaxProgress;
|
int remaining = objective.MaxProgress;
|
||||||
@@ -161,9 +164,9 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
} else if (reward.ItemID == "Aurum") {
|
} else if (reward.ItemID == "Aurum") {
|
||||||
balance.HardCurrency += reward.Amount;
|
balance.HardCurrency += reward.Amount;
|
||||||
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
|
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
|
||||||
} else if (reward.ItemID == "InsuranceToken") {
|
} else if (reward.ItemID == "InsuranceCurrency") {
|
||||||
balance.InsuranceTokens += reward.Amount;
|
balance.InsuranceCurrency += reward.Amount;
|
||||||
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceToken", Amount = balance.InsuranceTokens });
|
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
|
||||||
} else {
|
} else {
|
||||||
// TODO: Inventory limit check
|
// TODO: Inventory limit check
|
||||||
|
|
||||||
@@ -211,14 +214,44 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
factionProgression += contract.ReputationIncrease;
|
factionProgression += contract.ReputationIncrease;
|
||||||
contractsActive.Contracts.RemoveAt(targetContractIdx);
|
contractsActive.Contracts.RemoveAt(targetContractIdx);
|
||||||
// Main contracts can be completed only once.
|
// Main contracts can be completed only once.
|
||||||
|
// Other contracts should be jobs.
|
||||||
|
var newContractIdOnBoard = "";
|
||||||
if (contract.IsMainContract) {
|
if (contract.IsMainContract) {
|
||||||
contractsCompleted.ContractsIDs.Add(request.ContractID);
|
contractsCompleted.ContractsIDs.Add(contract.Name);
|
||||||
|
} else {
|
||||||
|
var parts = contract.Name.Split('-');
|
||||||
|
var difficulty = parts[1];
|
||||||
|
|
||||||
|
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(titleData["LevelData"]);
|
||||||
|
var levels = levelsData[contract.Faction];
|
||||||
|
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"]);
|
||||||
|
var level = levels.Length - Array.FindIndex(levels, (int xp) => {
|
||||||
|
return factionProgression >= xp;
|
||||||
|
});
|
||||||
|
|
||||||
|
var factionBoard = jobBoardsData.Boards.Find(c => c.FactionId == contract.Faction);
|
||||||
|
var idx = Array.FindIndex(difficulties, d => d == difficulty);
|
||||||
|
var factionJobs = jobs[contract.Faction];
|
||||||
|
var jobsAvailable = Array.FindAll(factionJobs[difficulty], (string jobId) => {
|
||||||
|
return level >= contracts[jobId].UnlockData.Level;
|
||||||
|
});
|
||||||
|
// If player completes a job, it means he has at least one unlocked anyway.
|
||||||
|
newContractIdOnBoard = jobsAvailable[rnd.Next(jobsAvailable.Length)];
|
||||||
|
factionBoard.Contracts[idx].ContractId = newContractIdOnBoard;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Check for next unlock in contracts instead of relying on client
|
||||||
var newContracts = new List<FYActiveContractPlayerData>();
|
var newContracts = new List<FYActiveContractPlayerData>();
|
||||||
foreach (var contractIdToUnlock in request.ContractsToUnlock) {
|
foreach (var contractIdToUnlock in request.ContractsToUnlock) {
|
||||||
var contractToUnlock = contracts[contractIdToUnlock];
|
var contractToUnlock = contracts[contractIdToUnlock];
|
||||||
var progress = new int[contractToUnlock.Objectives.Length];
|
var progress = new int[contractToUnlock.Objectives.Length];
|
||||||
|
// TODO: Automatically set max progress of kill and visit objectives. To remove once kill and visit objectives are reported by the client.
|
||||||
|
for (var i = 0; i < contractToUnlock.Objectives.Length; i++) {
|
||||||
|
var objective = contractToUnlock.Objectives[i];
|
||||||
|
if (objective.Type != EYContractObjectiveType.OwnNumOfItem) {
|
||||||
|
progress[i] = objective.MaxProgress;
|
||||||
|
}
|
||||||
|
}
|
||||||
var newContract = new FYActiveContractPlayerData {
|
var newContract = new FYActiveContractPlayerData {
|
||||||
ContractID = contractIdToUnlock,
|
ContractID = contractIdToUnlock,
|
||||||
Progress = progress,
|
Progress = progress,
|
||||||
@@ -235,6 +268,7 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
["Balance"] = JsonSerializer.Serialize(balance),
|
["Balance"] = JsonSerializer.Serialize(balance),
|
||||||
["Inventory"] = JsonSerializer.Serialize(newInventory),
|
["Inventory"] = JsonSerializer.Serialize(newInventory),
|
||||||
[factionKey] = JsonSerializer.Serialize(factionProgression),
|
[factionKey] = JsonSerializer.Serialize(factionProgression),
|
||||||
|
["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -242,12 +276,12 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
|
|||||||
{
|
{
|
||||||
UserID = userId,
|
UserID = userId,
|
||||||
Error = "",
|
Error = "",
|
||||||
ClaimedContractID = request.ContractID,
|
ClaimedContractID = contract.Name,
|
||||||
ContractsActivated = newContracts,
|
ContractsActivated = newContracts,
|
||||||
ItemsUpdatedOrRemoved = itemsUpdatedOrRemoved,
|
ItemsUpdatedOrRemoved = itemsUpdatedOrRemoved,
|
||||||
ChangedCurrencies = changedCurrencies.ToArray(),
|
ChangedCurrencies = changedCurrencies.ToArray(),
|
||||||
ItemsGranted = itemsGranted,
|
ItemsGranted = itemsGranted,
|
||||||
NewContractIdOnBoard = "", // TODO: This should be a replacement contract apparently?
|
NewContractIdOnBoard = newContractIdOnBoard,
|
||||||
PlayerFactionProgressData = new FYPlayerFactionProgressData {
|
PlayerFactionProgressData = new FYPlayerFactionProgressData {
|
||||||
FactionID = contract.Faction,
|
FactionID = contract.Faction,
|
||||||
CurrentProgression = factionProgression,
|
CurrentProgression = factionProgression,
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Prospect.Server.Api.Hubs;
|
||||||
|
using Prospect.Server.Api.Services.CloudScript.Models;
|
||||||
|
|
||||||
|
// Closed Beta Function
|
||||||
|
|
||||||
|
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
||||||
|
|
||||||
|
[CloudScriptFunction("EnterMatchmaking")]
|
||||||
|
public class EnterMatchmakingFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchAzureFunctionResult>
|
||||||
|
{
|
||||||
|
private readonly IHubContext<CycleHub> _hubContext;
|
||||||
|
|
||||||
|
public EnterMatchmakingFunction(IHubContext<CycleHub> hubContext)
|
||||||
|
{
|
||||||
|
_hubContext = hubContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<FYEnterMatchAzureFunctionResult> ExecuteAsync(FYEnterMatchAzureFunction request)
|
||||||
|
{
|
||||||
|
//await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
|
||||||
|
// Success = true,
|
||||||
|
// SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
|
||||||
|
// SquadID = request.SquadId,
|
||||||
|
//});
|
||||||
|
|
||||||
|
return new FYEnterMatchAzureFunctionResult
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
ErrorMessage = "",
|
||||||
|
SingleplayerStation = true,
|
||||||
|
Address = request.MapName,
|
||||||
|
MaintenanceMode = false,
|
||||||
|
Port = 7777,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
using Prospect.Server.Api.Hubs;
|
using Prospect.Server.Api.Hubs;
|
||||||
|
using Prospect.Server.Api.Services.Auth.Extensions;
|
||||||
using Prospect.Server.Api.Services.CloudScript.Models;
|
using Prospect.Server.Api.Services.CloudScript.Models;
|
||||||
|
using Prospect.Server.Api.Services.UserData;
|
||||||
|
|
||||||
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
namespace Prospect.Server.Api.Services.CloudScript.Functions;
|
||||||
|
|
||||||
@@ -17,15 +20,72 @@ public class OnSquadMatchmakingSuccessMessage {
|
|||||||
[CloudScriptFunction("EnterMatchmakingMatch")]
|
[CloudScriptFunction("EnterMatchmakingMatch")]
|
||||||
public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
|
public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
|
||||||
{
|
{
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly IHubContext<CycleHub> _hubContext;
|
private readonly IHubContext<CycleHub> _hubContext;
|
||||||
|
private readonly UserDataService _userDataService;
|
||||||
|
private readonly TitleDataService _titleDataService;
|
||||||
|
|
||||||
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext)
|
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
|
||||||
{
|
{
|
||||||
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_hubContext = hubContext;
|
_hubContext = hubContext;
|
||||||
|
_userDataService = userDataService;
|
||||||
|
_titleDataService = titleDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
|
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
|
||||||
{
|
{
|
||||||
|
var context = _httpContextAccessor.HttpContext;
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new CloudScriptException("CloudScript was not called within a http request");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = context.User.FindAuthUserId();
|
||||||
|
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
|
||||||
|
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
|
||||||
|
|
||||||
|
var userData = await _userDataService.FindAsync(
|
||||||
|
userId, userId,
|
||||||
|
new List<string>{"ContractsActive", "Inventory"}
|
||||||
|
);
|
||||||
|
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
|
||||||
|
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
||||||
|
// Compute delivery quest progress before deploy.
|
||||||
|
// This ensures that the quest progress will be shown on the planet.
|
||||||
|
// The station doesn't seem to care about delivery quests progress and calculates progress
|
||||||
|
// based on actual items in stash. And so does the "ClaimActiveContract" function.
|
||||||
|
foreach (var contractActive in contractsActive.Contracts) {
|
||||||
|
if (!contracts.TryGetValue(contractActive.ContractID, out var contract)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < contract.Objectives.Length; i++) {
|
||||||
|
var objective = contract.Objectives[i];
|
||||||
|
if (objective.Type != EYContractObjectiveType.OwnNumOfItem) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int remaining = objective.MaxProgress;
|
||||||
|
foreach (var item in inventory) {
|
||||||
|
if (item.BaseItemId != objective.ItemToOwn) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
remaining -= item.Amount;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
remaining = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contractActive.Progress[i] = objective.MaxProgress - remaining;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await _userDataService.UpdateAsync(
|
||||||
|
userId, userId,
|
||||||
|
new Dictionary<string, string>{
|
||||||
|
["ContractsActive"] = JsonSerializer.Serialize(contractsActive),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
|
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
|
||||||
Success = true,
|
Success = true,
|
||||||
SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
|
SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
|
||||||
|
|||||||
+71
-70
@@ -1,4 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Collections;
|
||||||
|
using System.Diagnostics.Contracts;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Prospect.Server.Api.Models.Data;
|
||||||
using Prospect.Server.Api.Services.Auth.Extensions;
|
using Prospect.Server.Api.Services.Auth.Extensions;
|
||||||
using Prospect.Server.Api.Services.CloudScript.Models;
|
using Prospect.Server.Api.Services.CloudScript.Models;
|
||||||
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
||||||
@@ -11,11 +14,16 @@ public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQuery
|
|||||||
{
|
{
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly UserDataService _userDataService;
|
private readonly UserDataService _userDataService;
|
||||||
|
private readonly TitleDataService _titleDataService;
|
||||||
|
private readonly string[] factions = ["Korolev", "Osiris", "ICA"]; // TODO: Order is important
|
||||||
|
private readonly string[] difficulties = ["Easy", "Medium", "Hard"];
|
||||||
|
private readonly Random rnd = new Random();
|
||||||
|
|
||||||
public RequestActiveObjectivesAndBoardsData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
|
public RequestActiveObjectivesAndBoardsData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
|
||||||
{
|
{
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_userDataService = userDataService;
|
_userDataService = userDataService;
|
||||||
|
_titleDataService = titleDataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYQueryFactionProgressionRequest request)
|
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYQueryFactionProgressionRequest request)
|
||||||
@@ -26,10 +34,65 @@ public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQuery
|
|||||||
return new FYGetPlayerContractsResult{};
|
return new FYGetPlayerContractsResult{};
|
||||||
}
|
}
|
||||||
var userId = context.User.FindAuthUserId();
|
var userId = context.User.FindAuthUserId();
|
||||||
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"ContractsActive"});
|
|
||||||
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
|
||||||
|
|
||||||
// TODO: Get player's contract boards and refresh
|
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{ "ContractsActive", "JobBoardsData", "FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris" });
|
||||||
|
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
|
||||||
|
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value);
|
||||||
|
|
||||||
|
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
var lastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc;
|
||||||
|
var boards = jobBoardsData.Boards;
|
||||||
|
// The boards are updated once in 3 hours
|
||||||
|
if (now >= lastBoardRefreshTimeUtc.Seconds + 60 * 60 * 3) {
|
||||||
|
var titleData = _titleDataService.Find(new List<string> { "Contracts", "Jobs", "LevelData" });
|
||||||
|
|
||||||
|
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
|
||||||
|
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"]);
|
||||||
|
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(titleData["LevelData"]);
|
||||||
|
var currentQuests = new Dictionary<string, Dictionary<string, bool>> {
|
||||||
|
["Korolev"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
|
||||||
|
["Osiris"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
|
||||||
|
["ICA"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var contract in contractsActive.Contracts) {
|
||||||
|
var parts = contract.ContractID.Split('-');
|
||||||
|
if (parts[0] != "NEW") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var difficulty = parts[1];
|
||||||
|
var faction = parts[2] == "KOR" ? "Korolev" : parts[2];
|
||||||
|
currentQuests[faction][difficulty] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < factions.Length; i++) {
|
||||||
|
var faction = factions[i];
|
||||||
|
var factionProgression = JsonSerializer.Deserialize<int>(userData[$"FactionProgression{faction}"].Value);
|
||||||
|
var levels = levelsData[faction];
|
||||||
|
var level = levels.Length - Array.FindIndex(levels, (int xp) => {
|
||||||
|
return factionProgression >= xp;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (var j = 0; j < difficulties.Length; j++) {
|
||||||
|
var difficulty = difficulties[j];
|
||||||
|
if (currentQuests[faction][difficulty]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var factionJobs = jobs[faction];
|
||||||
|
var jobsAvailable = Array.FindAll(factionJobs[difficulty], (string jobId) => {
|
||||||
|
return level >= contracts[jobId].UnlockData.Level;
|
||||||
|
});
|
||||||
|
string jobId = jobsAvailable.Length > 0 ? jobsAvailable[rnd.Next(jobsAvailable.Length)] : factionJobs[difficulty][0];
|
||||||
|
boards[i].Contracts[j] = new FYFactionContractData { ContractId = jobId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastBoardRefreshTimeUtc.Seconds = now;
|
||||||
|
|
||||||
|
await _userDataService.UpdateAsync(
|
||||||
|
userId, userId,
|
||||||
|
new Dictionary<string, string> { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new FYGetPlayerContractsResult
|
return new FYGetPlayerContractsResult
|
||||||
{
|
{
|
||||||
@@ -38,72 +101,10 @@ public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQuery
|
|||||||
ActiveContracts = contractsActive.Contracts,
|
ActiveContracts = contractsActive.Contracts,
|
||||||
FactionsContracts = new FYFactionsContractsData
|
FactionsContracts = new FYFactionsContractsData
|
||||||
{
|
{
|
||||||
Boards = new List<FYFactionContractsData>
|
Boards = boards,
|
||||||
{
|
LastBoardRefreshTimeUtc = lastBoardRefreshTimeUtc
|
||||||
new FYFactionContractsData
|
|
||||||
{
|
|
||||||
FactionId = "ICA",
|
|
||||||
Contracts = new List<FYFactionContractData>
|
|
||||||
{
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Easy-ICA-Gather-1",
|
|
||||||
},
|
},
|
||||||
new FYFactionContractData
|
RefreshHours24UtcFromBackend = 12 // Doesn't seem to work
|
||||||
{
|
|
||||||
ContractId = "NEW-Medium-ICA-Uplink-1",
|
|
||||||
},
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Hard-ICA-Uplink-1",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new FYFactionContractsData
|
|
||||||
{
|
|
||||||
FactionId = "Korolev",
|
|
||||||
Contracts = new List<FYFactionContractData>
|
|
||||||
{
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Easy-KOR-Mine-4",
|
|
||||||
},
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Medium-KOR-Mine-1",
|
|
||||||
},
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Hard-KOR-PvP-6",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new FYFactionContractsData
|
|
||||||
{
|
|
||||||
FactionId = "Osiris",
|
|
||||||
Contracts = new List<FYFactionContractData>
|
|
||||||
{
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Easy-Osiris-Brightcaps-1",
|
|
||||||
},
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Medium-Osiris-Gather-1",
|
|
||||||
},
|
|
||||||
new FYFactionContractData
|
|
||||||
{
|
|
||||||
ContractId = "NEW-Hard-Osiris-Gather-7",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LastBoardRefreshTimeUtc = new FYTimestamp
|
|
||||||
{
|
|
||||||
Seconds = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RefreshHours24UtcFromBackend = 12
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,9 +74,9 @@ public class RequestMissionCompleted : ICloudScriptFunction<FYRequestMissionComp
|
|||||||
} else if (reward.RewardRowHandle.RowName == "Aurum") {
|
} else if (reward.RewardRowHandle.RowName == "Aurum") {
|
||||||
balance.HardCurrency += reward.RewardAmount;
|
balance.HardCurrency += reward.RewardAmount;
|
||||||
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
|
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
|
||||||
} else if (reward.RewardRowHandle.RowName == "InsuranceToken") {
|
} else if (reward.RewardRowHandle.RowName == "InsuranceCurrency") {
|
||||||
balance.InsuranceTokens += reward.RewardAmount;
|
balance.InsuranceCurrency += reward.RewardAmount;
|
||||||
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceToken", Amount = balance.InsuranceTokens });
|
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
|
||||||
} else {
|
} else {
|
||||||
// TODO: Inventory limit check
|
// TODO: Inventory limit check
|
||||||
var blueprintData = blueprints[reward.RewardRowHandle.RowName];
|
var blueprintData = blueprints[reward.RewardRowHandle.RowName];
|
||||||
|
|||||||
+2
@@ -82,6 +82,7 @@ public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<Reques
|
|||||||
// 2. Stack existing items - deletes existing items and updates amount of existing item.
|
// 2. Stack existing items - deletes existing items and updates amount of existing item.
|
||||||
// 3. Change vanity data - updates an existing item.
|
// 3. Change vanity data - updates an existing item.
|
||||||
// 4. Change weapon mods - updates an existing item.
|
// 4. Change weapon mods - updates an existing item.
|
||||||
|
// IMPORTANT: The same request is currently used to report the updated inventory in the end of match.
|
||||||
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
|
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
|
||||||
foreach (var item in inventory) {
|
foreach (var item in inventory) {
|
||||||
if (!request.ItemsToRemove.Contains(item.ItemId)) {
|
if (!request.ItemsToRemove.Contains(item.ItemId)) {
|
||||||
@@ -98,6 +99,7 @@ public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<Reques
|
|||||||
inventoryItem.ModData = item.ModData;
|
inventoryItem.ModData = item.ModData;
|
||||||
inventoryItem.PrimaryVanityId = item.PrimaryVanityId;
|
inventoryItem.PrimaryVanityId = item.PrimaryVanityId;
|
||||||
inventoryItem.SecondaryVanityId = item.SecondaryVanityId;
|
inventoryItem.SecondaryVanityId = item.SecondaryVanityId;
|
||||||
|
inventoryItem.Durability = item.Durability;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check updated items and validate new items
|
// TODO: Check updated items and validate new items
|
||||||
|
|||||||
@@ -117,11 +117,11 @@ public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientReque
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var blueprintData = blueprints[inventoryItem.BaseItemId];
|
var blueprintData = blueprints[inventoryItem.BaseItemId];
|
||||||
// TODO: Probably better to decide based on item kind instead
|
// NOTE: Ammo and consumables have reverse price mapping.
|
||||||
if (inventoryItem.Durability == -1) {
|
if (blueprintData.Kind == "Ammo" || blueprintData.Kind == "Ability") {
|
||||||
factionProgression += (int)((float)blueprintData.OverrideScrappingReputation / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
|
factionProgression += (int)((float)blueprintData.OverrideScrappingReputation / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
|
||||||
balance.SoftCurrency += (int)((float)blueprintData.OverrideScrappingReturns / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
|
balance.SoftCurrency += (int)((float)blueprintData.OverrideScrappingReturns / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
|
||||||
} else {
|
} else if (blueprintData.Kind == "Armor") {
|
||||||
factionProgression += (int)Math.Max(
|
factionProgression += (int)Math.Max(
|
||||||
(float)inventoryItem.Durability / blueprintData.DurabilityMax * blueprintData.OverrideScrappingReputation,
|
(float)inventoryItem.Durability / blueprintData.DurabilityMax * blueprintData.OverrideScrappingReputation,
|
||||||
blueprintData.OverrideScrappingReputation * blueprintData.DurabilityBrokenScrappingReturnModifier
|
blueprintData.OverrideScrappingReputation * blueprintData.DurabilityBrokenScrappingReturnModifier
|
||||||
@@ -130,6 +130,9 @@ public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientReque
|
|||||||
(float)inventoryItem.Durability / blueprintData.DurabilityMax * blueprintData.OverrideScrappingReturns,
|
(float)inventoryItem.Durability / blueprintData.DurabilityMax * blueprintData.OverrideScrappingReturns,
|
||||||
blueprintData.OverrideScrappingReturns * blueprintData.DurabilityBrokenScrappingReturnModifier
|
blueprintData.OverrideScrappingReturns * blueprintData.DurabilityBrokenScrappingReturnModifier
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
factionProgression += blueprintData.OverrideScrappingReputation * inventoryItem.Amount;
|
||||||
|
balance.SoftCurrency += blueprintData.OverrideScrappingReturns * inventoryItem.Amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrappedItemIds.Add(itemId);
|
scrappedItemIds.Add(itemId);
|
||||||
|
|||||||
@@ -29,3 +29,30 @@ public class FYEnterMatchAzureFunction
|
|||||||
[JsonPropertyName("loginNonce")]
|
[JsonPropertyName("loginNonce")]
|
||||||
public string LoginNonce { get; set; }
|
public string LoginNonce { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FYEnterMatchAzureFunctionCB
|
||||||
|
{
|
||||||
|
[JsonPropertyName("optimalRegion")]
|
||||||
|
public string? OptimalRegion { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isMatch")]
|
||||||
|
public bool IsMatch { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("bypassMaintenanceMode")]
|
||||||
|
public bool BypassMaintenanceMode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("debugOption")]
|
||||||
|
public string? DebugOption { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("mapName")]
|
||||||
|
public string? MapName { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("squad_id")]
|
||||||
|
public string? SquadId { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("purchaseInsuranceRequest")]
|
||||||
|
public FYPurchaseInventoryInsuranceRequest PurchaseInventoryInsurances { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("loginNonce")]
|
||||||
|
public string LoginNonce { get; set; }
|
||||||
|
}
|
||||||
+27
@@ -21,3 +21,30 @@ public class FYEnterMatchmakingResult
|
|||||||
[JsonPropertyName("errorMessage")]
|
[JsonPropertyName("errorMessage")]
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FYEnterMatchAzureFunctionResult
|
||||||
|
{
|
||||||
|
[JsonPropertyName("success")]
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("address")]
|
||||||
|
public string? Address { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("errorMessage")]
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("port")]
|
||||||
|
public int Port { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sharedIndex")]
|
||||||
|
public int ShardIndex { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("singleplayerStation")]
|
||||||
|
public bool SingleplayerStation { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("maintenanceMode")]
|
||||||
|
public bool MaintenanceMode { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("sessionTimeJoinDelay")]
|
||||||
|
public float SessionTimeJoinDelay { get; set; }
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Prospect.Server.Api.Models.Client.Data;
|
using Prospect.Server.Api.Models.Client.Data;
|
||||||
|
using Prospect.Server.Api.Models.Data;
|
||||||
using Prospect.Server.Api.Services.CloudScript.Functions;
|
using Prospect.Server.Api.Services.CloudScript.Functions;
|
||||||
|
using Prospect.Server.Api.Services.CloudScript.Models.Data;
|
||||||
using Prospect.Server.Api.Services.Database;
|
using Prospect.Server.Api.Services.Database;
|
||||||
|
|
||||||
namespace Prospect.Server.Api.Services.UserData;
|
namespace Prospect.Server.Api.Services.UserData;
|
||||||
@@ -171,6 +173,25 @@ public class UserDataService
|
|||||||
["GlobalVanity"] = (false, $"{{\"activeGlobalVanityIds\":[\"Season03_Spray_13\",\"Banner_MIne\",\"Emote_Chill_01\",\"\",\"\",\"\",\"\",\"\"], \"droppodId\": \"VDP_Omega02\"}}"),
|
["GlobalVanity"] = (false, $"{{\"activeGlobalVanityIds\":[\"Season03_Spray_13\",\"Banner_MIne\",\"Emote_Chill_01\",\"\",\"\",\"\",\"\",\"\"], \"droppodId\": \"VDP_Omega02\"}}"),
|
||||||
["CharacterVanity"] = (false, JsonSerializer.Serialize(defaultVanity)),
|
["CharacterVanity"] = (false, JsonSerializer.Serialize(defaultVanity)),
|
||||||
["Inventory"] = (false, "[]"),
|
["Inventory"] = (false, "[]"),
|
||||||
|
["JobBoardsData"] = (false, JsonSerializer.Serialize(
|
||||||
|
new JobBoardsData {
|
||||||
|
LastBoardRefreshTimeUtc = new CloudScript.Models.Data.FYTimestamp { Seconds = 0 },
|
||||||
|
Boards = [
|
||||||
|
new FYFactionContractsData {
|
||||||
|
FactionId = "Korolev",
|
||||||
|
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
|
||||||
|
},
|
||||||
|
new FYFactionContractsData {
|
||||||
|
FactionId = "Osiris",
|
||||||
|
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
|
||||||
|
},
|
||||||
|
new FYFactionContractsData {
|
||||||
|
FactionId = "ICA",
|
||||||
|
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)),
|
||||||
["VanityItems"] = (false, "[]"),
|
["VanityItems"] = (false, "[]"),
|
||||||
["RetentionBonus"] = (false, "{\"claimedAll\":false,\"daysClaimed\":0,\"lastClaimTime\":{\"seconds\":0}}"),
|
["RetentionBonus"] = (false, "{\"claimedAll\":false,\"daysClaimed\":0,\"lastClaimTime\":{\"seconds\":0}}"),
|
||||||
["Balance"] = (false, "{\"IN\": 0,\"AU\": 800,\"SC\": 30000}"),
|
["Balance"] = (false, "{\"IN\": 0,\"AU\": 800,\"SC\": 30000}"),
|
||||||
|
|||||||
+26
-26
@@ -21,7 +21,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Tests", "Pr
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Generator", "Prospect.Unreal.Generator\Prospect.Unreal.Generator.csproj", "{71E3E262-49C5-4B6C-9670-D0741CEDB63B}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Generator", "Prospect.Unreal.Generator\Prospect.Unreal.Generator.csproj", "{71E3E262-49C5-4B6C-9670-D0741CEDB63B}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "Prospect.Client.Loader\Prospect.Client.Loader.vcxproj", "{8170A17F-E536-4FDB-BC01-78BF09A9A48B}"
|
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "Prospect.Client.Loader\Prospect.Client.Loader.vcxproj", "{04B1D963-2BCB-4887-9D6D-04CB3366A348}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
@@ -183,30 +183,30 @@ Global
|
|||||||
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
|
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
|
||||||
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
|
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
|
||||||
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
|
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Debug|x64.ActiveCfg = Debug|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Debug|x64.Build.0 = Debug|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x64.Build.0 = Debug|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Debug|x86.ActiveCfg = Debug|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x86.ActiveCfg = Debug|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Debug|x86.Build.0 = Debug|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x86.Build.0 = Debug|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Release|x64.ActiveCfg = Release|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x64.ActiveCfg = Release|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Release|x64.Build.0 = Release|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x64.Build.0 = Release|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Release|x86.ActiveCfg = Release|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x86.ActiveCfg = Release|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Release|x86.Build.0 = Release|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x86.Build.0 = Release|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Debug|x64.ActiveCfg = Debug|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x64.ActiveCfg = Debug|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Debug|x64.Build.0 = Debug|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x64.Build.0 = Debug|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Debug|x86.ActiveCfg = Debug|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x86.ActiveCfg = Debug|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Debug|x86.Build.0 = Debug|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x86.Build.0 = Debug|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Release|x64.ActiveCfg = Release|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x64.ActiveCfg = Release|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Release|x64.Build.0 = Release|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x64.Build.0 = Release|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Release|x86.ActiveCfg = Release|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x86.ActiveCfg = Release|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 2 Release|x86.Build.0 = Release|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x86.Build.0 = Release|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Debug|x64.ActiveCfg = Debug|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x64.ActiveCfg = Debug|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Debug|x64.Build.0 = Debug|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x64.Build.0 = Debug|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Debug|x86.ActiveCfg = Debug|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x86.ActiveCfg = Debug|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Debug|x86.Build.0 = Debug|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x86.Build.0 = Debug|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Release|x64.ActiveCfg = Release|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x64.ActiveCfg = Release|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Release|x64.Build.0 = Release|x64
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x64.Build.0 = Release|x64
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Release|x86.ActiveCfg = Release|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.ActiveCfg = Release|Win32
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B}.Season 3 Release|x86.Build.0 = Release|Win32
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.Build.0 = Release|Win32
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -218,7 +218,7 @@ Global
|
|||||||
{380DD490-8F01-4025-91D7-74C289AA5B75} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
{380DD490-8F01-4025-91D7-74C289AA5B75} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
||||||
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA}
|
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA}
|
||||||
{71E3E262-49C5-4B6C-9670-D0741CEDB63B} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
{71E3E262-49C5-4B6C-9670-D0741CEDB63B} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
|
||||||
{8170A17F-E536-4FDB-BC01-78BF09A9A48B} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
|
{04B1D963-2BCB-4887-9D6D-04CB3366A348} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {12888C62-D3A2-4DD7-A9A5-1481D95865D0}
|
SolutionGuid = {12888C62-D3A2-4DD7-A9A5-1481D95865D0}
|
||||||
|
|||||||
Reference in New Issue
Block a user