6 Commits
b4 ... b6
Author SHA1 Message Date
Yury 840efb8383 Add Prospect.Client.Loader and update Solution config 2025-03-04 23:56:50 +02:00
Yury c3dbfa5d59 Update instructions 2025-03-04 23:56:37 +02:00
Yury dfc38de7b5 Push API changes
* Fix item repair cost formula
* Add "RequestUpdateAvailableFreeLoadout" function (WIP)
* Fix item sell formula
* Fix item stack split on sell
* Fix contract progression counting for objectives other than owned items
* Refactor balance into a proper struct
* Separate and update Season 2 and Season 3 data
2025-03-04 23:39:20 +02:00
YuryandGitHub 08ca12f1e3 Update README.md 2025-02-24 11:03:32 +02:00
YuryandGitHub fb9acbbedc Update README.md 2025-02-24 11:01:37 +02:00
Yury 280b87314c Push changes 2025-02-24 01:06:32 +02:00
135 changed files with 6547 additions and 1193 deletions
+102 -55
View File
@@ -1,52 +1,76 @@
# Prospect # Prospect <!-- omit in toc -->
Also known as "The Cycle: Frontier". Also known as "The Cycle: Frontier".
## Table of Contents <!-- omit in toc -->
- [Features](#features)
- [Running locally](#running-locally)
- [1. Prerequisites](#1-prerequisites)
- [1.1 How to download Season 2 client from SteamDB using Steam console](#11-how-to-download-season-2-client-from-steamdb-using-steam-console)
- [2. Unpack `Prospect.Server.Api`](#2-unpack-prospectserverapi)
- [3. Patch the `hosts` file](#3-patch-the-hosts-file)
- [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)
- [6. Copy the `Prospect.Client.Loader` to the game](#6-copy-the-prospectclientloader-to-the-game)
- [7. Run the server](#7-run-the-server)
- [8. Run the game](#8-run-the-game)
- [Troubleshooting and FAQ](#troubleshooting-and-faq)
- [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)
- [Body parts are missing with Season 3 client](#body-parts-are-missing-with-season-3-client)
- [Prospect.Server.Api does not start](#prospectserverapi-does-not-start)
- [Login Failed. Error code: 3](#login-failed-error-code-3)
- [Login Failed. Error code: 5](#login-failed-error-code-5)
- [Development](#development)
## Features ## Features
* [x] Basic login with Steam. * [x] Basic login with Steam
* [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):
* Basic station functionality:
* [ ] Onboarding * [ ] Onboarding
* [x] Talk to Badum * [x] Talk to Badum
* [x] Equip items * [x] Equip items
* [ ] Deploy to Fortuna * [ ] Deploy to Fortuna
* [ ] Matchmaking * [ ] Matchmaking and deployment
* [x] Can deploy through matchmaking * [x] Solo
* [ ] Squad
* [ ] Items insurance * [ ] Items insurance
* [ ] Free loadouts (Season 3) * [ ] Free loadouts (Season 3)
* [ ] Gameplay * [x] Inventory and loadout
* [ ] Can evac * [ ] Loadout presets (Season 3)
* [ ] Can do quests * [x] Quests
* [ ] Inventory and loadout
* [x] Items can be equipped
* [x] Items can be unequipped.
* [x] Loadout is carried into matchmaking
* [x] Items are automatically stacked
* [x] Weapon mods can be equipped
* [x] Weapon appearance can be changed
* [ ] Quests
* [x] Quests can be accepted
* [ ] Quests can be removed
* [ ] Quests can be completed
* [ ] Job boards * [ ] Job boards
* [x] Faction progression * [x] Faction progression
* [ ] Season pass * [ ] Season pass
* [x] Character appearance and emotes
* [ ] Shops * [ ] Shops
* [x] Items can be bought * [ ] Daily shop
* [x] Items can be sold (balance and progression is received) * [ ] Weekly shop
* [ ] Crafting station * [ ] Shop rotation
* [ ] Season pass * [x] Daily login
* [ ] Quarters * [x] Character appearance and emotes
* [ ] Generator * [x] Shops
* [ ] Quarter upgrades * [x] Crafting station
* [x] Quarters
* [x] Player balance * [x] Player balance
* [x] K-Marks * [ ] Social features
* [x] Aurum * [ ] Proximity voice
* [x] Insurance Tokens * [x] Vivox login
* [x] Vivox create and join channel
* [ ] Proximity voice works
* [ ] Game mechanics
* [x] Can deploy through terminal
* [x] Can deploy with loadout
* [ ] Can evac
* [ ] Can do quests
* [ ] Can gain/lose loot
* [ ] Can use Alien Forge.
* [ ] Map content
* [x] Bright Sands
* [ ] Crescent Falls
* [ ] Tharis Island
## Running locally ## Running locally
@@ -55,24 +79,51 @@ Also known as "The Cycle: Frontier".
### 1. Prerequisites ### 1. Prerequisites
> [!WARNING]
> The latest Steam version of The Cycle: Frontier currently does not work with Windows 11 24H2!
> [!IMPORTANT]
> You must have The Cycle: Frontier from Steam in your Steam library to be able to download it.
> Otherwise, the download will fail with an error message about missing license.
Before you start, you'll need the following software downloaded and installed: 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. [Latest `Prospect.Server.Api`](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.
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. 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.
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. [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. A compatible The Cycle: Frontier game client. The server works with: 1. The Cycle: Frontier game client:
1. The latest version from [Steam](https://steamcommunity.com/app/868270). - The latest version from [Steam](https://steamcommunity.com/app/868270).
> [!WARNING] - Season 2 client. See [download instructions below](#11-how-to-download-season-2-client-from-steamdb-using-steam-console).
> The latest version of The Cycle: Frontier does not work with Windows 11 24H2!
1. Season 2 client downloaded from SteamDB using a depot downloader. For example, [version 2.7.2](https://steamdb.info/depot/868271/history/?changeid=M:4623363103423775682). - Any other version, even Season 1 works.
#### 1.1 How to download Season 2 client from SteamDB using Steam console
> [!WARNING]
> This will overwrite the existing client if you try to download a different manifest!
1. With Steam running, press `Win+R` and enter `steam://nav/console`. A Steam console will open.
1. Open [The Cycle: Frontier SteamDB manifests](https://steamdb.info/depot/868271/manifests/).
1. Make sure you have **Copy format** set to **Steam console**.
1. Press `CTRL+F` and enter `4623363103423775682` to find the manifest for Season 2 version 2.7.2 client.
1. Click the ![Copy](./_assets/steamdb_copy.PNG) icon to copy the download command.
1. Paste the command in the Steam console and press `Enter`.
1. The depot will begin downloading. You should receive a notification and the destination folder when the download is complete.
### 2. Unpack `Prospect.Server.Api` ### 2. Unpack `Prospect.Server.Api`
@@ -110,7 +161,7 @@ to successfully communicate with the local server. Do the following:
1. Select **Place all certificates in the following store** > **Browse...**. Choose **Trusted Root Certification Authorities** and click **OK**. Click **Next**. 1. Select **Place all certificates in the following store** > **Browse...**. Choose **Trusted Root Certification Authorities** and click **OK**. Click **Next**.
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. Add `steam_appid` to the game
@@ -122,17 +173,13 @@ to successfully communicate with the local server. Do the following:
1. Save and close the file. 1. Save and close the file.
### 6. Create a game shortcut with specified arguments ### 6. Copy the `Prospect.Client.Loader` 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 `Prospect-Win64-Shipping.exe` > **Create Shortcut**. 1. Copy the downloaded `Prospect.Client.Loader` file into the folder.
1. Right-click the created shortcut > **Properties**. 1. Create a shortcut for the `Prospect.Client.Loader` that you will use later to launch the game.
1. In the **Target** field, add the following parameters after the quote ` -empty -server -log -windowed -noeac -nointro -steam_auth PF_TITLEID=2EA46`. Note the space after the quote.
1. Move the shortcut to desktop.
### 7. Run the server ### 7. Run the server
@@ -147,9 +194,9 @@ Now you are all set! Open the folder with `Prospect.Server.Api` and run `Prospec
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.
### Troubleshooting and FAQ ## Troubleshooting and FAQ
#### How to remove the certificate? ### How to remove the certificate?
If you've installed the certificate for the **Current User**: If you've installed the certificate for the **Current User**:
@@ -161,20 +208,20 @@ If you've installed the certificate for the **Current User**:
If you've installed the certificate for the **Local Machine**, repeat the same steps but instead open `certlm.msc`. If you've installed the certificate for the **Local Machine**, repeat the same steps but instead open `certlm.msc`.
#### `generate_ssl.exe` is flagged as a virus ### `generate_ssl.exe` is flagged as a virus
`generate_ssl.exe` is a Python application packed with PyInstaller and some anti-viruses may flag it as a virus. `generate_ssl.exe` is a Python application packed with PyInstaller and some anti-viruses may flag it as a virus.
This application is a simple certificate generator and you can find its source code in `utils/generate_ssl.py`. This application is a simple certificate generator and you can find its source code in `utils/generate_ssl.py`.
#### Body parts are missing with Season 3 client ### Body parts are missing with Season 3 client
Currently, the server loads body part IDs for Season 2 by default, so this is expected. You can fix this by going to station and changing your character appearance. This will store the updated body part IDs for your character. Currently, the server loads body part IDs for Season 2 by default, so this is expected. You can fix this by going to station and changing your character appearance. This will store the updated body part IDs for your character.
#### Prospect.Server.Api does not start ### Prospect.Server.Api does not start
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. 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.
#### Login Failed. Error code: 3 ### Login Failed. Error code: 3
Make sure that: Make sure that:
@@ -182,14 +229,14 @@ Make sure that:
* You have created and **saved** the `steam_appid` file as described in step 6. * You have created and **saved** the `steam_appid` file as described in step 6.
* The `steam_appid` file type is "TXT File". * The `steam_appid` file type is "TXT File".
#### Login Failed. Error code: 5 ### Login Failed. Error code: 5
Make sure that: Make sure that:
* `Prospect.Server.Api` server is running. * `Prospect.Server.Api` server is running.
* The `C:\Windows\System32\drivers\etc\hosts` file contains `127.0.0.1 2EA46.playfabapi.com` and that the file is **saved**. * The `C:\Windows\System32\drivers\etc\hosts` file contains `127.0.0.1 2EA46.playfabapi.com` and that the file is **saved**.
If the server works and the `hosts` file is saved, `Alt+Tab` to a game console that opens when you start the game and check for the following: If the server works and the `hosts` file is saved, press `Alt+Tab` to a game console that opens when you start the game and check for the following:
* `libcurl error 7 (Couldn't connect to server)` - indicates that the `Prospect.Server.Api` is not running. * `libcurl error 7 (Couldn't connect to server)` - indicates that the `Prospect.Server.Api` is not running.
![](./_assets/connection_refused_error.png) ![](./_assets/connection_refused_error.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

-105
View File
@@ -1,105 +0,0 @@
#pragma once
#include <cstdint>
#include <locale>
#include <string>
#include "SDK_Memory.h"
namespace SDK
{
template<class T>
struct TArray
{
friend struct FString;
TArray()
{
Data = nullptr;
Count = Max = 0;
}
int Num() const
{
return Count;
}
T& operator[](int i)
{
return Data[i];
}
const T& operator[](int i) const
{
return Data[i];
}
bool IsValidIndex(int i) const
{
return i < Num();
}
private:
T* Data;
int32_t Count;
int32_t Max;
};
struct FString : private TArray<wchar_t>
{
FString(const wchar_t* other)
{
const size_t charLen = std::wcslen(other);
if (charLen == 0)
{
Count = 0;
Max = 0;
Data = nullptr;
}
else
{
Count = charLen + 1;
Max = Count;
Data = static_cast<wchar_t*>((*GMalloc)->Malloc(Count * 2));
memset(Data, 0, Count * 2);
memcpy(Data, other, charLen * 2);
}
}
FString(const std::wstring& other) : FString(other.c_str())
{
}
bool IsValid() const
{
return Data != nullptr;
}
const wchar_t* c_str() const
{
return Data;
}
std::string ToString() const
{
const auto length = std::wcslen(Data);
std::string str(length, '\0');
std::use_facet<std::ctype<wchar_t>>(std::locale()).narrow(Data, Data + length, '?', &str[0]);
return str;
}
};
struct UPlayFabAPISettings
{
FString VerticalName;
FString BaseServiceHost;
FString TitleId;
FString AdvertisingIdType;
FString AdvertisingIdValue;
bool DisableAdvertising = false;
};
}
-79
View File
@@ -1,79 +0,0 @@
#pragma once
#include <cstdint>
#define DEFAULT_ALIGNMENT 0
namespace SDK
{
class FExec
{
public:
virtual ~FExec() = default;
private:
virtual bool Exec(void* world, void* cmd, void* ar) = 0;
};
class FMalloc : FExec
{
public:
virtual void* Malloc(SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
virtual void* TryMalloc(SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
virtual void* Realloc(void* Original, SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
virtual void* TryRealloc(void* Original, SIZE_T Count, uint32_t Alignment = DEFAULT_ALIGNMENT) = 0;
virtual void Free(void* Original) = 0;
virtual SIZE_T QuantizeSize(SIZE_T Count, uint32_t Alignment)
{
return Count;
}
virtual bool GetAllocationSize(void* Original, SIZE_T& SizeOut)
{
return false;
}
virtual void Trim(bool bTrimThreadCaches)
{
}
virtual void SetupTLSCachesOnCurrentThread()
{
}
virtual void ClearAndDisableTLSCachesOnCurrentThread()
{
}
virtual void InitializeStatsMetadata();
virtual bool Exec(void* InWorld, void* Cmd, void* Ar) override
{
return false;
}
virtual void UpdateStats() = 0;
virtual void GetAllocatorStats(void* out_Stats) = 0;
virtual void DumpAllocatorStats(class FOutputDevice& Ar) = 0;
virtual bool IsInternallyThreadSafe() const
{
return false;
}
virtual bool ValidateHeap()
{
return(true);
}
virtual const TCHAR* GetDescriptiveName() = 0;
};
FMalloc** GMalloc;
}
-3
View File
@@ -1,3 +0,0 @@
#pragma once
#include "SDK.h"
-85
View File
@@ -1,85 +0,0 @@
#include "logger.h"
#include <cstdio>
#include <Windows.h>
HANDLE h_out = nullptr, h_old_out = nullptr;
HANDLE h_err = nullptr, h_old_err = nullptr;
HANDLE h_in = nullptr, h_old_in = nullptr;
namespace logger
{
void Attach()
{
h_old_out = GetStdHandle(STD_OUTPUT_HANDLE);
h_old_err = GetStdHandle(STD_ERROR_HANDLE);
h_old_in = GetStdHandle(STD_INPUT_HANDLE);
AllocConsole() && AttachConsole(GetCurrentProcessId());
h_out = GetStdHandle(STD_OUTPUT_HANDLE);
h_err = GetStdHandle(STD_ERROR_HANDLE);
h_in = GetStdHandle(STD_INPUT_HANDLE);
SetConsoleMode(h_out,
ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT);
SetConsoleMode(h_in,
ENABLE_INSERT_MODE | ENABLE_EXTENDED_FLAGS |
ENABLE_PROCESSED_INPUT | ENABLE_QUICK_EDIT_MODE);
}
void Detach()
{
if (h_out && h_err && h_in) {
FreeConsole();
if (h_old_out)
{
SetStdHandle(STD_OUTPUT_HANDLE, h_old_out);
}
if (h_old_err)
{
SetStdHandle(STD_ERROR_HANDLE, h_old_err);
}
if (h_old_in)
{
SetStdHandle(STD_INPUT_HANDLE, h_old_in);
}
}
}
bool Print(const char* fmt, ...)
{
if (!h_out)
{
return false;
}
char buf[1024];
va_list va;
va_start(va, fmt);
_vsnprintf_s(buf, 1024, fmt, va);
va_end(va);
return !!WriteConsoleA(h_out, buf, static_cast<DWORD>(strlen(buf)), nullptr, nullptr);
}
char ReadKey()
{
if (!h_in)
{
return -1;
}
auto key = char{ 0 };
auto keysread = DWORD{ 0 };
ReadConsoleA(h_in, &key, 1, &keysread, nullptr);
return key;
}
};
-12
View File
@@ -1,12 +0,0 @@
#pragma once
namespace logger
{
void Attach();
void Detach();
bool Print(const char* fmt, ...);
char ReadKey();
}
-101
View File
@@ -1,101 +0,0 @@
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <MinHook.h>
#include <cstdio>
#include "logger.h"
#include "SDK.h"
#include "SDK_Memory.h"
// Pointers are for 211025.1143
// https://steamdb.info/depot/1600361/history/?changeid=M:720055916602660127
/**
* Variable offset, find with "STOPMOVIECAPTURE", scroll up.
*/
constexpr uintptr_t OffsetGMalloc = 0x5C9D478;
/**
* Function offset, find with "?sdk=".
*/
constexpr uintptr_t OffsetPlayFabApiGetUrl = 0xCA5010;
/**
* Function hooks.
*/
typedef SDK::FString(__fastcall* tPlayFabApiGetUrl)(SDK::UPlayFabAPISettings*, const SDK::FString&);
tPlayFabApiGetUrl OldPlayFabApiGetUrl;
SDK::FString __fastcall PlayFabApiGetUrlProxy(SDK::UPlayFabAPISettings* thiz, const SDK::FString& callPath)
{
logger::Print("[Agent] GetURL called with callPath %s\n", callPath.ToString().c_str());
return std::wstring(L"http://127.0.0.1:5000") + std::wstring(callPath.c_str());
}
DWORD WINAPI OnDllAttach(LPVOID base)
{
logger::Print("[Agent] DLL Attached\n");
const auto hModule = GetModuleHandleW(nullptr);
const auto hModulePtr = reinterpret_cast<uintptr_t>(hModule);
// Initialize GMalloc.
SDK::GMalloc = reinterpret_cast<SDK::FMalloc**>(hModulePtr + OffsetGMalloc);
// Initialize MinHook.
if (MH_Initialize() != MH_OK)
{
logger::Print("[Agent] Failed to initialize MinHook\n");
FreeLibraryAndExitThread(hModule, 1);
}
// Hook UPlayFabAPISettings::GetUrl.
const auto pTarget = reinterpret_cast<LPVOID>(hModulePtr + OffsetPlayFabApiGetUrl);
const auto ppOriginal = reinterpret_cast<LPVOID*>(&OldPlayFabApiGetUrl);
const auto mhStatus = MH_CreateHook(pTarget, &PlayFabApiGetUrlProxy, ppOriginal);
if (mhStatus != MH_OK)
{
logger::Print("[Agent] Failed to create PlayFabAPIGetUrl hook (%d)\n", mhStatus);
FreeLibraryAndExitThread(hModule, 1);
}
if (MH_EnableHook(pTarget) != MH_OK)
{
logger::Print("[Agent] Failed to enable PlayFabAPIGetUrl hook\n");
FreeLibraryAndExitThread(hModule, 1);
}
logger::Print("[Agent] DLL Initialized\n");
ExitThread(1);
}
BOOL WINAPI DllMain(
const _In_ HINSTANCE hinstDll,
const _In_ DWORD fdwReason,
const _In_opt_ LPVOID lpvReserved
)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hinstDll);
// Allocate a console.
AllocConsole();
freopen_s(reinterpret_cast<FILE**>(stdout), "CONOUT$", "w", stdout);
// Attach logger.
logger::Attach();
// Spawn thread.
CreateThread(nullptr, 0, OnDllAttach, hinstDll, 0, nullptr);
}
else if (fdwReason == DLL_PROCESS_DETACH)
{
// Detach logger.
logger::Detach();
}
return TRUE;
}
@@ -1,14 +0,0 @@
{
"default-registry": {
"kind": "git",
"baseline": "2960d7d80e8d09c84ae8abf15c12196c2ca7d39a",
"repository": "https://github.com/microsoft/vcpkg"
},
"registries": [
{
"kind": "artifact",
"location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip",
"name": "microsoft"
}
]
}
-5
View File
@@ -1,5 +0,0 @@
{
"dependencies": [
"minhook"
]
}
@@ -1,6 +1,14 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations"> <ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64"> <ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration> <Configuration>Debug</Configuration>
<Platform>x64</Platform> <Platform>x64</Platform>
@@ -11,21 +19,35 @@
</ProjectConfiguration> </ProjectConfiguration>
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Globals"> <PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion> <VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword> <Keyword>Win32Proj</Keyword>
<ProjectGuid>{a9ba7d25-f239-4320-a0dc-85e8001fd669}</ProjectGuid> <ProjectGuid>{8170a17f-e536-4fdb-bc01-78bf09a9a48b}</ProjectGuid>
<RootNamespace>ProspectAgent</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">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType> <ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries> <UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset> <PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet> <CharacterSet>Unicode</CharacterSet>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType> <ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries> <UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset> <PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization> <WholeProgramOptimization>true</WholeProgramOptimization>
@@ -36,6 +58,12 @@
</ImportGroup> </ImportGroup>
<ImportGroup Label="Shared"> <ImportGroup Label="Shared">
</ImportGroup> </ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup> </ImportGroup>
@@ -43,42 +71,44 @@
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup> </ImportGroup>
<PropertyGroup Label="UserMacros" /> <PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<LinkIncremental>true</LinkIncremental> <ClCompile>
<OutDir>$(ProjectDir)..\Prospect.Launcher\</OutDir> <WarningLevel>Level3</WarningLevel>
<IntDir>$(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\</IntDir> <SDLCheck>true</SDLCheck>
<LibraryPath>$(LibraryPath)</LibraryPath> <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</PropertyGroup> <ConformanceMode>true</ConformanceMode>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> </ClCompile>
<LinkIncremental>false</LinkIncremental> <Link>
<OutDir>$(ProjectDir)..\Prospect.Launcher\</OutDir> <SubSystem>Console</SubSystem>
<IntDir>$(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\</IntDir> <GenerateDebugInformation>true</GenerateDebugInformation>
<LibraryPath>$(LibraryPath)</LibraryPath> </Link>
</PropertyGroup> </ItemDefinitionGroup>
<PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<VcpkgUseStatic>true</VcpkgUseStatic> <ClCompile>
</PropertyGroup> <WarningLevel>Level3</WarningLevel>
<PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <FunctionLevelLinking>true</FunctionLevelLinking>
<VcpkgUseStatic>true</VcpkgUseStatic> <IntrinsicFunctions>true</IntrinsicFunctions>
</PropertyGroup> <SDLCheck>true</SDLCheck>
<PropertyGroup Label="Vcpkg"> <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<VcpkgEnableManifest>true</VcpkgEnableManifest> <ConformanceMode>true</ConformanceMode>
</PropertyGroup> </ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile> <ClCompile>
<WarningLevel>Level3</WarningLevel> <WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck> <SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem>Windows</SubSystem> <SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
@@ -87,30 +117,17 @@
<FunctionLevelLinking>true</FunctionLevelLinking> <FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions> <IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck> <SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions> <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode> <ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>MinSpace</Optimization>
</ClCompile> </ClCompile>
<Link> <Link>
<SubSystem>Windows</SubSystem> <SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding> <EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences> <OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation> <GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="hooks.h" />
<ClInclude Include="logger.h" />
<ClInclude Include="SDK.h" />
<ClInclude Include="SDK_Memory.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="logger.cpp" />
<ClCompile Include="main.cpp" /> <ClCompile Include="main.cpp" />
</ItemGroup> </ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
@@ -14,26 +14,9 @@
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions> <Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter> </Filter>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ClInclude Include="logger.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SDK.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="hooks.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="SDK_Memory.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="main.cpp"> <ClCompile Include="main.cpp">
<Filter>Source Files</Filter> <Filter>Source Files</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="logger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup> </ItemGroup>
</Project> </Project>
+139
View File
@@ -0,0 +1,139 @@
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// Function to create and launch a process with parameters
HANDLE StartProcess(const wchar_t* exePath, DWORD* processID) {
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi = { 0 };
si.cb = sizeof(si);
// Command-line arguments to pass to the process
wchar_t commandLine[MAX_PATH] = L""; // Buffer for full command
_snwprintf_s(commandLine, sizeof(commandLine) / sizeof(wchar_t), L"\"%s\" -log -steam_auth PF_TITLEID=2EA46", exePath);
if (CreateProcess(NULL, commandLine, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
*processID = pi.dwProcessId;
printf("Successfully started process: %ls (PID: %lu)\n", exePath, *processID);
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
return pi.hProcess;
}
else {
printf("Failed to start process. Error: %lu\n", GetLastError());
return NULL;
}
}
// Function to get the base address of the main module (PE Image)
uintptr_t GetModuleBaseAddress(DWORD processID) {
uintptr_t baseAddress = 0;
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, processID);
if (hSnapshot != INVALID_HANDLE_VALUE) {
MODULEENTRY32 moduleEntry;
moduleEntry.dwSize = sizeof(MODULEENTRY32);
if (Module32First(hSnapshot, &moduleEntry)) {
baseAddress = (uintptr_t)moduleEntry.modBaseAddr;
}
CloseHandle(hSnapshot);
}
else {
printf("Failed to take process snapshot. Error: %lu\n", GetLastError());
}
return baseAddress;
}
// Function to get a handle to the target process
HANDLE OpenTargetProcess(DWORD processID) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processID);
if (hProcess == NULL) {
printf("Failed to open process. Error: %lu\n", GetLastError());
}
return hProcess;
}
bool CheckTutorialActorsOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0x32 && buffer[1] == 0xDB;
} else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckTutorialActorsOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool Patch(const wchar_t* processName) {
DWORD processID;
uintptr_t imageBase, address;
BYTE newValues[2] = { 0xB3, 0x01 };
SIZE_T bytesWritten;
HANDLE hProcess = StartProcess(processName, &processID);
if (hProcess == NULL) {
return false;
}
// Wait a bit for PE image to load
Sleep(5000);
// Find the PE Image Base Address
imageBase = GetModuleBaseAddress(processID);
if (imageBase == 0) {
printf("Failed to find PE Image Base Address.\n");
return false;
}
printf("PE Image Base Address: 0x%p\n", (void*)imageBase);
// Season 3 Patch tutorial actors loader: sub_3DA7520 -> loc_3DA75B6
address = imageBase + 0x3DA75B6;
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;
}
DWORD oldProtect;
if (VirtualProtectEx(hProcess, (LPVOID)address, sizeof(newValues), PAGE_EXECUTE_READWRITE, &oldProtect)) {
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
CloseHandle(hProcess);
return true;
}
int main() {
bool success = Patch(L"Prospect-Win64-Shipping.exe");
if (!success) {
// Wait for user input to exit
printf("An error occurred when patching the executable! Press any key to exit...\n");
getchar();
return 1;
}
return 0;
}
-55
View File
@@ -1,55 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Prospect.Launcher.Invoke;
namespace Prospect.Launcher;
public static class Inject
{
const int PROCESS_CREATE_THREAD = 0x0002;
const int PROCESS_QUERY_INFORMATION = 0x0400;
const int PROCESS_VM_OPERATION = 0x0008;
const int PROCESS_VM_WRITE = 0x0020;
const int PROCESS_VM_READ = 0x0010;
const uint MEM_COMMIT = 0x00001000;
const uint MEM_RESERVE = 0x00002000;
const uint PAGE_READWRITE = 4;
// https://codingvision.net/c-inject-a-dll-into-a-process-w-createremotethread
public static unsafe bool Library(uint processId, string path)
{
IntPtr procHandle = Kernel32.OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, false, (int)processId);
var loadLibraryAddr = Kernel32.GetProcAddress(Kernel32.GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (loadLibraryAddr == IntPtr.Zero)
{
Console.WriteLine("Failed GetProcAddress.");
return false;
}
var allocMemAddress = Kernel32.VirtualAllocEx(procHandle, IntPtr.Zero, (uint)((path.Length + 1) * Marshal.SizeOf(typeof(char))), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (allocMemAddress == IntPtr.Zero)
{
Console.WriteLine("Failed VirtualAllocEx.");
return false;
}
var libraryBytes = Encoding.Default.GetBytes(path);
fixed (byte* pLibraryBytes = libraryBytes)
{
Kernel32.WriteProcessMemory(procHandle, allocMemAddress, pLibraryBytes, libraryBytes.Length + 1, out _);
}
var threadHandle = Kernel32.CreateRemoteThread(procHandle, IntPtr.Zero, 0, loadLibraryAddr, allocMemAddress, 0, IntPtr.Zero);
if (threadHandle == IntPtr.Zero)
{
Console.WriteLine("Failed CreateRemoteThread.");
return false;
}
return true;
}
}
-52
View File
@@ -1,52 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Prospect.Launcher.Invoke.Structs;
namespace Prospect.Launcher.Invoke;
internal static class Kernel32
{
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CreateProcess(string lpApplicationName,
string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles, ProcessCreationFlags dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory,
ref StartupInfo lpStartupInfo,
out ProcessInformation lpProcessInformation);
public static bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
ProcessCreationFlags dwCreationFlags,
ref StartupInfo lpStartupInfo,
out ProcessInformation lpProcessInformation) => CreateProcess(lpApplicationName, lpCommandLine, IntPtr.Zero,
IntPtr.Zero, false, dwCreationFlags, IntPtr.Zero, null, ref lpStartupInfo, out lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern unsafe bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte* lpBuffer,
Int32 nSize,
out IntPtr lpNumberOfBytesWritten
);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
}
@@ -1,25 +0,0 @@
using System;
namespace Prospect.Launcher.Invoke.Structs;
[Flags]
internal enum ProcessCreationFlags : uint
{
ZERO_FLAG = 0x00000000,
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00001000,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
INHERIT_PARENT_AFFINITY = 0x00010000
}
@@ -1,13 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Prospect.Launcher.Invoke.Structs;
[StructLayout(LayoutKind.Sequential)]
internal struct ProcessInformation
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
@@ -1,27 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Prospect.Launcher.Invoke.Structs;
[StructLayout(LayoutKind.Sequential)]
internal struct StartupInfo
{
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
-77
View File
@@ -1,77 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using Prospect.Launcher.Invoke;
using Prospect.Launcher.Invoke.Structs;
namespace Prospect.Launcher;
internal static class Program
{
/// <summary>
/// Steam AppId.
/// - https://steamdb.info/app/480/
/// - https://steamdb.info/app/1600360/
/// </summary>
private const string AppId = "480";
/// <summary>
/// Executable name of the game.
/// </summary>
private const string FileName = "Prospect-Win64-Shipping.exe";
private static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Usage: ./Prospect.Launcher <Game directory>");
return;
}
var gamePath = Path.Combine(args[0], "Prospect", "Binaries", "Win64");
var gameBinary = Path.Combine(gamePath, FileName);
var gameArgs = new List<string>();
// 2EA46 = Default
// A22AB = The Cycle Playtest
gameArgs.Add("-empty");
gameArgs.Add("-log");
gameArgs.Add("-windowed");
gameArgs.Add("-noeac");
gameArgs.Add("-nointro");
gameArgs.Add("-steam_auth");
gameArgs.Add("PF_TITLEID=A22AB");
// Ensure "steam_appid.txt" exists, to fix steam authentication.
var steamAppId = Path.Combine(gamePath, "steam_appid.txt");
File.WriteAllText(steamAppId, AppId);
// Spawn.
var startupInfo = new StartupInfo();
if (!Kernel32.CreateProcess(gameBinary, string.Join(' ', gameArgs), ProcessCreationFlags.CREATE_SUSPENDED, ref startupInfo, out var processInfo))
{
Console.WriteLine("Failed to spawn game.");
return;
}
Console.WriteLine("Spawned.");
// Inject DLL.
var agentPath = Path.Combine(AppContext.BaseDirectory, "Prospect.Agent.dll");
if (!Inject.Library(processInfo.dwProcessId, agentPath))
{
Console.WriteLine("Failed to inject library.");
return;
}
Console.WriteLine("Injected.");
// Resume.
Kernel32.ResumeThread(processInfo.hThread);
Console.WriteLine("Resumed.");
}
}
@@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Content Include="Prospect.Agent.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
</Project>
@@ -1,10 +1,12 @@
using System.Net.Mime; using System.Net.Mime;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Prospect.Server.Api.Config; using Prospect.Server.Api.Config;
using Prospect.Server.Api.Models.Client; using Prospect.Server.Api.Models.Client;
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.Auth; using Prospect.Server.Api.Services.Auth;
using Prospect.Server.Api.Services.Auth.Extensions; using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Auth.User; using Prospect.Server.Api.Services.Auth.User;
@@ -21,6 +23,7 @@ public class ClientController : Controller
{ {
private const int AppIdDefault = 480; private const int AppIdDefault = 480;
private const int AppIdCycleBeta = 1600361; private const int AppIdCycleBeta = 1600361;
private const int AppIdCycle = 868270;
private readonly ILogger<ClientController> _logger; private readonly ILogger<ClientController> _logger;
private readonly PlayFabSettings _settings; private readonly PlayFabSettings _settings;
@@ -55,7 +58,7 @@ public class ClientController : Controller
if (!string.IsNullOrEmpty(request.SteamTicket)) if (!string.IsNullOrEmpty(request.SteamTicket))
{ {
var ticket = AppTicket.Parse(request.SteamTicket); var ticket = AppTicket.Parse(request.SteamTicket);
if (ticket.IsValid && ticket.HasValidSignature && (ticket.AppId == AppIdDefault || ticket.AppId == AppIdCycleBeta)) if (ticket.IsValid && ticket.HasValidSignature && (ticket.AppId == AppIdDefault || ticket.AppId == AppIdCycleBeta || ticket.AppId == AppIdCycle))
{ {
var userSteamId = ticket.SteamId.ToString(); var userSteamId = ticket.SteamId.ToString();
@@ -147,6 +150,19 @@ public class ClientController : Controller
}); });
} }
[HttpPost("GetStoreItems")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetStoreItems(FGetStoreItems request)
{
return Ok(new ClientResponse<object>
{
Code = 200,
Status = "OK",
Data = new {}
});
}
[HttpPost("UpdateUserTitleDisplayName")] [HttpPost("UpdateUserTitleDisplayName")]
[Produces(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)] [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
@@ -205,15 +221,18 @@ public class ClientController : Controller
[HttpPost("GetUserReadOnlyData")] [HttpPost("GetUserReadOnlyData")]
[Produces(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)] [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetUserReadOnlyData(FGetUserDataRequest request) public async Task<IActionResult> GetUserReadOnlyData(FGetUserDataRequest request)
{ {
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, request.PlayFabId, request.Keys);
return Ok(new ClientResponse<FGetUserDataResult> return Ok(new ClientResponse<FGetUserDataResult>
{ {
Code = 200, Code = 200,
Status = "OK", Status = "OK",
Data = new FGetUserDataResult Data = new FGetUserDataResult
{ {
Data = new Dictionary<string, FUserDataRecord>(), Data = userData,
DataVersion = 0 DataVersion = 0
} }
}); });
@@ -222,40 +241,63 @@ public class ClientController : Controller
[HttpPost("GetUserInventory")] [HttpPost("GetUserInventory")]
[Produces(MediaTypeNames.Application.Json)] [Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)] [Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetUserReadOnlyData(FGetUserInventoryRequest request) public async Task<IActionResult> GetUserInventory(FGetUserInventoryRequest request)
{ {
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems"});
var inventory = JsonSerializer.Deserialize<FYCustomItemInfo[]>(userData["Inventory"].Value);
// TODO: Per-user vanity
// var vanity = JsonSerializer.Deserialize<CustomVanityItem[]>(userData["VanityItems"].Value);
var titleData = _titleDataService.Find(new List<string>{"Blueprints", "Vanities"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var vanity = JsonSerializer.Deserialize<TitleDataVanityInfo[]>(titleData["Vanities"]);
List<FItemInstance> items = new List<FItemInstance>();
foreach (var item in inventory) {
if (!blueprints.ContainsKey(item.BaseItemId)) {
_logger.LogWarning("Failed to find item with ID {ItemID}", item.BaseItemId);
continue;
}
var blueprintData = blueprints[item.BaseItemId];
items.Add(new FItemInstance {
ItemId = item.BaseItemId,
ItemInstanceId = item.ItemId,
ItemClass = blueprintData.Kind,
// PurchaseDate = DateTime.Now, // TODO
// UnitPrice = 0,
CatalogVersion = "StaticItems",
CustomData = new Dictionary<string, string> {
["mods"] = JsonSerializer.Serialize(item.ModData),
// ["insurance"] = "None", // TODO
["vanity"] = $"{{\"p\":{item.PrimaryVanityId},\"s\":{item.SecondaryVanityId}}}", // p - primary, s - secondary
["coreData"] = $"{{\"a\":{item.Amount},\"d\":{item.Durability}}}", // a - amount, d - durability
["origin"] = JsonSerializer.Serialize(item.Origin),
["rolledPerks"] = JsonSerializer.Serialize(item.RolledPerks),
}
});
}
foreach (var item in vanity) {
items.Add(new FItemInstance {
ItemId = item.Name,
// ItemInstanceId is not required since it's not a unique item.
ItemClass = "Vanity",
// PurchaseDate = DateTime.Now, // TODO
CatalogVersion = "StaticItems",
CustomData = new Dictionary<string, string> {
["coreData"] = "{}", // Vanity items don't seem to have any coreData. But it's still required since it's an inventory item.
["origin"] = "{}",
}
});
}
return Ok(new ClientResponse<FGetUserInventoryResult> return Ok(new ClientResponse<FGetUserInventoryResult>
{ {
Code = 200, Code = 200,
Status = "OK", Status = "OK",
Data = new FGetUserInventoryResult Data = new FGetUserInventoryResult
{ {
Inventory = new List<FItemInstance> Inventory = items,
{ VirtualCurrency = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value),
// new FItemInstance
// {
// ItemId = "Helmet_03",
// ItemInstanceId = "0102030405060708",
// ItemClass = "Helmet",
// PurchaseDate = DateTime.Now.AddDays(-1),
// CatalogVersion = "StaticItems",
// DisplayName = "Helmet",
// UnitPrice = 0,
// CustomData = new Dictionary<string, string>
// {
// ["insurance"] = "None",
// ["mods"] = "{\"m\":[]}",
// ["vanity_misc_data"] = "{\"v\":\"\",\"s\":\"\",\"l\":3,\"a\":1,\"d\":300}"
// }
// }
},
VirtualCurrency = new Dictionary<string, int>
{
["AE"] = 0,
["AS"] = 0,
["AU"] = int.MaxValue,
["SC"] = int.MaxValue
},
VirtualCurrencyRechargeTimes = new Dictionary<string, FVirtualCurrencyRechargeTime>() VirtualCurrencyRechargeTimes = new Dictionary<string, FVirtualCurrencyRechargeTime>()
} }
}); });
+5 -1
View File
@@ -4,5 +4,9 @@ namespace Prospect.Server.Api.Hubs;
public class CycleHub : Hub public class CycleHub : Hub
{ {
public override async Task OnConnectedAsync()
{
Console.WriteLine("Connected {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
} }
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
namespace Prospect.Server.Api.Models.Client;
public class FGetStoreItems
{
}
@@ -1,5 +1,6 @@
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;
namespace Prospect.Server.Api.Models.Client; namespace Prospect.Server.Api.Models.Client;
@@ -17,7 +18,7 @@ public class FGetUserInventoryResult
/// </summary> /// </summary>
[JsonPropertyName("VirtualCurrency")] [JsonPropertyName("VirtualCurrency")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Dictionary<string, int>? VirtualCurrency { get; set; } public PlayerBalance? VirtualCurrency { get; set; }
/// <summary> /// <summary>
/// [optional] Array of virtual currency balance(s) belonging to the user. /// [optional] Array of virtual currency balance(s) belonging to the user.
@@ -0,0 +1,15 @@
using System.Text.Json.Serialization;
namespace Prospect.Server.Api.Models.Data;
public class PlayerBalance
{
[JsonPropertyName("AU")]
public int HardCurrency { get; set; }
[JsonPropertyName("SC")]
public int SoftCurrency { get; set; }
[JsonPropertyName("IN")]
public int InsuranceTokens { get; set; }
}
@@ -0,0 +1,64 @@
using System.Text.Json.Serialization;
public class TitleDataContractInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Faction")]
public string Faction { get; set; }
[JsonPropertyName("ReputationIncrease")]
public int ReputationIncrease { get; set; }
[JsonPropertyName("Rewards")]
public TitleDataContractInfoReward[] Rewards { get; set; }
[JsonPropertyName("Objectives")]
public TitleDataContractInfoObjective[] Objectives { get; set; }
[JsonPropertyName("UnlockData")]
public TitleDataContractInfoUnlockData UnlockData { get; set; }
[JsonPropertyName("IsMainContract")]
public bool IsMainContract { get; set; }
[JsonPropertyName("ContractDifficulty")]
public EYContractDifficulty ContractDifficulty { get; set; }
}
public class TitleDataContractInfoReward {
[JsonPropertyName("ItemID")]
public string ItemID { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
public class TitleDataContractInfoObjective {
[JsonPropertyName("Type")]
public EYContractObjectiveType Type { get; set; }
[JsonPropertyName("MaxProgress")]
public int MaxProgress { get; set; }
[JsonPropertyName("ItemToOwn")]
public string ItemToOwn { get; set; }
}
public class TitleDataContractInfoUnlockData {
[JsonPropertyName("Level")]
public int Level { get; set; }
[JsonPropertyName("Contracts")]
public HashSet<string> Contracts { get; set; }
}
public enum EYContractObjectiveType {
Invalid = 0,
Kills = 1,
OwnNumOfItem = 2,
DeadDrop = 3,
VisitArea = 4,
LootContainer = 5,
FactionLevel = 6,
CompletedMission = 7,
MAX = 8
}
public enum EYContractDifficulty
{
Invalid = 0,
Easy = 1,
Medium = 2,
Hard = 3,
MAX = 4
};
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
public class TitleDataDailyCrateInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Weight")]
public float Weight { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
@@ -0,0 +1,142 @@
using System.Text.Json.Serialization;
public class TitleDataBlueprintInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("ItemShopsCraftingData")]
public Dictionary<string, TitleDataItemShopsCraftingData> ItemShopsCraftingData { get; set; }
[JsonPropertyName("AmountPerPurchase")]
public int AmountPerPurchase { get; set; }
[JsonPropertyName("DurabilityMax")]
public int DurabilityMax { get; set; }
[JsonPropertyName("DurabilityBrokenScrappingReturnModifier")]
public double DurabilityBrokenScrappingReturnModifier { get; set; }
[JsonPropertyName("RepairCostMaxDurability")]
public int RepairCostMaxDurability { get; set; }
[JsonPropertyName("RepairCostBase")]
public int RepairCostBase { get; set; }
[JsonPropertyName("RepairCostModifierBroken")]
public int RepairCostModifierBroken { get; set; }
[JsonPropertyName("ItemWeight")]
public double ItemWeight { get; set; }
[JsonPropertyName("ScrappingReturnDefault")]
public int ScrappingReturnDefault { get; set; }
[JsonPropertyName("OverrideScrappingReturns")]
public int OverrideScrappingReturns { get; set; }
[JsonPropertyName("OverrideScrappingReputation")]
public int OverrideScrappingReputation { get; set; }
[JsonPropertyName("ScrappingFactionProgressionIncrement")]
public int ScrappingFactionProgressionIncrement { get; set; }
[JsonPropertyName("UnlockData")]
public TitleDataItemUnlockData UnlockData { get; set; }
[JsonPropertyName("ModID")]
public int? ModID { get; set; }
[JsonPropertyName("AlienForgeCatalystToUpgradedItemMap")]
public TitleDataItemAlienForgeCatalystToUpgradedItemMap AlienForgeCatalystToUpgradedItemMap { get; set; }
[JsonPropertyName("Rarity")]
public int Rarity { get; set; }
[JsonPropertyName("Kind")]
public string Kind { get; set; }
[JsonPropertyName("MaxAmountPerStack")]
public int MaxAmountPerStack { get; set; }
}
public class TitleDataItemShopsCraftingData {
[JsonPropertyName("ItemRecipeIngredients")]
public List<TitleDataItemRecipeCostType> ItemRecipeIngredients { get; set; }
[JsonPropertyName("UpgradeTimeMinutes")]
public int UpgradeTimeMinutes { get; set; }
[JsonPropertyName("UpgradeTimeSeconds")]
public int UpgradeTimeSeconds { get; set; }
[JsonPropertyName("SkipCraftingMaxCost")]
public int SkipCraftingMaxCost { get; set; }
[JsonPropertyName("SkipOptionalCraftingMaxCost")]
public int SkipOptionalCraftingMaxCost { get; set; }
}
public class TitleDataItemRecipeCostType {
[JsonPropertyName("Item")]
public string Currency { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
public class TitleDataItemUnlockData {
[JsonPropertyName("FactionUnlockLevel")]
public int FactionUnlockLevel { get; set; }
[JsonPropertyName("ContractLockPurchase")]
public string ContractLockPurchase { get; set; }
[JsonPropertyName("ContractLockCrafting")]
public string ContractLockCrafting { get; set; }
}
public class TitleDataItemAlienForgeCatalystToUpgradedItemMap {
[JsonPropertyName("Catalyst")]
public string Catalyst { get; set; }
[JsonPropertyName("NextItem")]
public string NextItem { get; set; }
}
/*
enum class EYItemType : uint8_t
{
EYItemType__None = 0,
EYItemType__Device = 1,
EYItemType__Ability = 2,
EYItemType__Kit = 3,
EYItemType__Consumable = 4,
EYItemType__Mod = 5,
EYItemType__Blueprint = 6,
EYItemType__Material = 7,
EYItemType__Miscellaneous = 8,
EYItemType__Currency = 9,
EYItemType__Vanity = 10,
EYItemType__Experience = 11,
EYItemType__Lore = 12,
EYItemType__Vehicle = 13,
EYItemType__Ammo = 14,
EYItemType__Upgrade = 15,
EYItemType__Collectible = 16,
EYItemType__QuestItem = 17,
EYItemType__Shield = 18,
EYItemType__ProspectorBadge = 19,
EYItemType__TechTreeNode = 20,
EYItemType__PlayerQuartersLevel = 21,
EYItemType__PassiveGenerator = 22,
EYItemType__Bag = 23,
EYItemType__Helmet = 24,
EYItemType__Key = 25,
EYItemType__MeleeWeapon = 26,
EYItemType__All = 27,
EYItemType__MAX = 28
};
enum class EYItemOriginType : uint8_t
{
EYItemOriginType__Undefined = 0,
EYItemOriginType__Contract = 1,
EYItemOriginType__Craft = 2,
EYItemOriginType__Creature = 3,
EYItemOriginType__Debug = 4,
EYItemOriginType__FortunaPass = 5,
EYItemOriginType__FTUE = 6,
EYItemOriginType__Generator = 7,
EYItemOriginType__Insurance = 8,
EYItemOriginType__MapContainerLoot = 9,
EYItemOriginType__MapPickupLoot = 10,
EYItemOriginType__MatchSplitStack = 11,
EYItemOriginType__MatchUnknown = 12,
EYItemOriginType__PlayFabDashboard = 13,
EYItemOriginType__PurchaseICA = 14,
EYItemOriginType__PurchaseKorolev = 15,
EYItemOriginType__PurchaseOsiris = 16,
EYItemOriginType__PurchaseQuickShop = 17,
EYItemOriginType__Retention = 18,
EYItemOriginType__StationSplitStack = 19,
EYItemOriginType__TocMigration = 20,
EYItemOriginType__TwitchDrop = 21,
EYItemOriginType__Uplink = 22,
EYItemOriginType__WeaponAttachment = 23,
EYItemOriginType__EYItemOriginType_MAX = 24
};
*/
@@ -0,0 +1,46 @@
using System.Text.Json.Serialization;
public class TitleDataMissionInfo {
[JsonPropertyName("MissionId")]
public string MissionId { get; set; }
[JsonPropertyName("NextMissionRowHandle")]
public UDataTable NextMissionRowHandle { get; set; }
[JsonPropertyName("OnboardingRewards")]
public UDataTable OnboardingRewards { get; set; }
}
// {\"TalkToBadumReward\":{\"MissionRewardId\":\"TalkToBadumReward\",\"RewardEntries\":[{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_SMG_Bullet_01\"},\"RewardAmount\":2},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_AR_Energy_01\"},\"RewardAmount\":1},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_Pistol_Bullet_01\"},\"RewardAmount\":5},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_SGun_Bullet_01\"},\"RewardAmount\":2},{\"RewardType\":6,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/AmmoTypes_DT.AmmoTypes_DT\\u0027\",\"RowName\":\"Light\"},\"RewardAmount\":6},{\"RewardType\":6,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/AmmoTypes_DT.AmmoTypes_DT\\u0027\",\"RowName\":\"Medium\"},\"RewardAmount\":4},{\"RewardType\":6,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/AmmoTypes_DT.AmmoTypes_DT\\u0027\",\"RowName\":\"Shotgun\"},\"RewardAmount\":2},{\"RewardType\":8,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Abilities.PRO_Abilities\\u0027\",\"RowName\":\"ShockGrenade_02\"},\"RewardAmount\":2},{\"RewardType\":8,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Abilities.PRO_Abilities\\u0027\",\"RowName\":\"Consumable_Health_01\"},\"RewardAmount\":5},{\"RewardType\":0,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_PlayerShield.PRO_PlayerShield\\u0027\",\"RowName\":\"Shield_01\"},\"RewardAmount\":5},{\"RewardType\":10,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Bags.PRO_Bags\\u0027\",\"RowName\":\"Bag_01\"},\"RewardAmount\":5},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"TOOL_MineralScanner_01\"},\"RewardAmount\":2}]},\"FirstMatchReward\":{\"MissionRewardId\":\"FirstMatchReward\",\"RewardEntries\":[{\"RewardType\":2,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Materials_DT.Materials_DT\\u0027\",\"RowName\":\"Insulation\"},\"RewardAmount\":4},{\"RewardType\":1,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Currencies_DT.Currencies_DT\\u0027\",\"RowName\":\"SoftCurrency\"},\"RewardAmount\":1500},{\"RewardType\":2,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Materials_DT.Materials_DT\\u0027\",\"RowName\":\"GenericCreatureDrop\"},\"RewardAmount\":3},{\"RewardType\":2,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Materials_DT.Materials_DT\\u0027\",\"RowName\":\"ICAScrip\"},\"RewardAmount\":1}]}}",
public class TitleDataMissionRewardInfo {
[JsonPropertyName("MissionRewardId")]
public string MissionRewardId { get; set; }
[JsonPropertyName("RewardEntries")]
public TitleDataMissionRewardEntry[] RewardEntries { get; set; }
}
public class TitleDataMissionRewardEntry {
[JsonPropertyName("RewardType")]
public int RewardType { get; set; }
[JsonPropertyName("RewardRowHandle")]
public UDataTable RewardRowHandle { get; set; }
[JsonPropertyName("RewardAmount")]
public int RewardAmount { get; set; }
}
/*
enum class EYRewardType : uint8_t
{
EYRewardType__None = 0,
EYRewardType__Currency = 1,
EYRewardType__Material = 2,
EYRewardType__Reputation = 3,
EYRewardType__SeasonXP = 4,
EYRewardType__ProspectorLevel = 5,
EYRewardType__Ammo = 6,
EYRewardType__Weapon = 7,
EYRewardType__Consumable = 8,
EYRewardType__Armor = 9,
EYRewardType__Bag = 10,
EYRewardType__MAX = 11
};
*/
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
public class TitleDataPassiveGeneratorsInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("GeneratorID")]
public string GeneratorID { get; set; }
[JsonPropertyName("BaseGenIntervalMinutes")]
public int BaseGenIntervalMinutes { get; set; }
[JsonPropertyName("BaseGenRate")]
public int BaseGenRate { get; set; }
[JsonPropertyName("BaseCap")]
public int BaseCap { get; set; }
}
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
public class TitleDataPlayerQuartersInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("NodeUpgradesRequired")]
public int NodeUpgradesRequired { get; set; }
[JsonPropertyName("UpgradeCosts")]
public TitleDataPlayerQuartersUpgradeCosts[] UpgradeCosts { get; set; }
[JsonPropertyName("UpgradeSeconds")]
public int UpgradeSeconds { get; set; }
[JsonPropertyName("InitialRushCosts")]
public int InitialRushCosts { get; set; }
[JsonPropertyName("OptionalRushCosts")]
public int OptionalRushCosts { get; set; }
}
public class TitleDataPlayerQuartersUpgradeCosts {
[JsonPropertyName("Currency")]
public string Currency { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
public class TitleDataRetentionBonusInfo {
[JsonPropertyName("Rewards")]
public string[] Rewards { get; set; }
[JsonPropertyName("Active")]
public bool Active { get; set; }
}
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript.Functions;
public class TitleDataTechTreeNodeInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("PlayerQuarterLevelRequired")]
public int PlayerQuarterLevelRequired { get; set; }
[JsonPropertyName("NodePerkType")]
public EYTechTreeNodePerkType NodePerkType { get; set; }
[JsonPropertyName("PerkLevels")]
public TitleDataTechTreeNodePerkLevels[] PerkLevels { get; set; }
}
public class TitleDataTechTreeNodePerkLevels {
[JsonPropertyName("PerkAmount")]
public float PerkAmount { get; set; }
[JsonPropertyName("UpgradeCosts")]
public TitleDataTechTreeNodePerkLevelsUpgradeCosts[] UpgradeCosts { get; set; }
[JsonPropertyName("UpgradeSeconds")]
public int UpgradeSeconds { get; set; }
[JsonPropertyName("InitialRushCosts")]
public int InitialRushCosts { get; set; }
[JsonPropertyName("OptionalRushCosts")]
public int OptionalRushCosts { get; set; }
}
public class TitleDataTechTreeNodePerkLevelsUpgradeCosts {
[JsonPropertyName("Currency")]
public string Currency { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
public class TitleDataTechTreeNodeUpgradeDependencies {
[JsonPropertyName("RelatedDependency")]
public string RelatedDependency { get; set; }
[JsonPropertyName("RequiredLevel")]
public int RequiredLevel { get; set; }
}
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
public class TitleDataVanityInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("StoreData")]
public TitleDataVanityInfoStoreData StoreData { get; set; }
}
public class TitleDataVanityInfoStoreData {
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
public class UDataTable {
[JsonPropertyName("DataTable")]
public string DataTable { get; set; }
[JsonPropertyName("RowName")]
public string RowName { get; set; }
}
@@ -0,0 +1,28 @@
{
"profiles": {
"Prospect.Server.Api": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production",
"ASPNETCORE_Kestrel__Certificates__Default__Path": ".\\certificate.pfx"
},
"dotnetRunMessages": true,
"applicationUrl": "https://2EA46.playfabapi.com:443",
"useSSL": true
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"sslPort": 0
}
}
}
@@ -1,13 +1,27 @@
{ {
"profiles": { "profiles": {
"Prospect.Server.Api": { "Prospect.Server.Api": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": "true", "environmentVariables": {
"launchBrowser": false, "ASPNETCORE_ENVIRONMENT": "Development",
"applicationUrl": "http://localhost:5000;https://localhost:5443", "ASPNETCORE_Kestrel__Certificates__Default__Path": ".\\certificate.pfx"
},
"dotnetRunMessages": true,
"applicationUrl": "https://2EA46.playfabapi.com"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
} }
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"sslPort": 0
}
} }
} }
@@ -4,6 +4,23 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings> <ImplicitUsings>true</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Season 3 Debug|AnyCPU'">
<Optimize>False</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Season 3 Release|AnyCPU'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Season 2 Release|AnyCPU'">
<Optimize>True</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<Optimize>True</Optimize>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -32,7 +32,10 @@ public class CloudScriptService
using (var scope = _serviceScopeFactory.CreateScope()) using (var scope = _serviceScopeFactory.CreateScope())
{ {
var functionClazz = ActivatorUtilities.CreateInstance(scope.ServiceProvider, function.Clazz); var functionClazz = ActivatorUtilities.CreateInstance(scope.ServiceProvider, function.Clazz);
var functionParam = JsonSerializer.Deserialize(parameter, function.RequestType); // Some functions have empty parameters which result in malformed JSON.
var parameterJson = parameter == "" ? "{}" : parameter;
// _logger.LogInformation("Received parameter: {Param}", parameterJson);
var functionParam = JsonSerializer.Deserialize(parameterJson, function.RequestType);
if (functionParam == null) if (functionParam == null)
{ {
_logger.LogWarning("Function {Function} deserialization returned null", name); _logger.LogWarning("Function {Function} deserialization returned null", name);
@@ -0,0 +1,94 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYActivateContractRequest {
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
}
public class FYActivateContractResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("changedCurrencties")]
public object[] ChangedCurrencties { get; set; }
[JsonPropertyName("activatedContract")]
public FYActiveContractPlayerData ActivatedContract { get; set; }
[JsonPropertyName("status")]
public int Status { get; set; }
}
public class FYGetActiveContractsResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("contracts")]
public List<FYActiveContractPlayerData> Contracts { get; set; }
}
[CloudScriptFunction("ActivateContract")]
public class ActivateContract : ICloudScriptFunction<FYActivateContractRequest, FYActivateContractResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public ActivateContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYActivateContractResult> ExecuteAsync(FYActivateContractRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"ContractsActive"});
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var contract = contracts[request.ContractID];
if (contract == null) {
return new FYActivateContractResult
{
UserID = userId,
Error = "Contract not found",
Status = 3 // EYActivateContractRequestStatus::FAILED_GETTING_STATIC_DATA
};
}
// TODO: Contract unlock criteria
var progress = new int[contract.Objectives.Length];
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var contractData = new FYActiveContractPlayerData {
ContractID = request.ContractID,
Progress = progress,
};
contractsActive.Contracts.Add(contractData);
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["ContractsActive"] = JsonSerializer.Serialize(contractsActive) }
);
return new FYActivateContractResult
{
UserID = userId,
Error = "",
ChangedCurrencties = [], // NOTE: There are no paid contracts in Frontier.
ActivatedContract = contractData,
Status = 14 // EYActivateContractRequestStatus::OK
};
}
}
@@ -0,0 +1,43 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CanGoToStationRequestRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; }
}
public class CanGoToStationRequestResponse
{
[JsonPropertyName("canGoToStation")]
public bool CanGoToStation { get; set; }
[JsonPropertyName("delta")]
public int Delta { get; set; }
[JsonPropertyName("timeStampUpdate")]
public int TimeStampUpdate { get; set; }
}
[CloudScriptFunction("CanGoToStationRequest")]
public class CanGoToStationRequestFunction : ICloudScriptFunction<CanGoToStationRequestRequest, CanGoToStationRequestResponse>
{
private readonly ILogger<CanGoToStationRequestFunction> _logger;
public CanGoToStationRequestFunction(ILogger<CanGoToStationRequestFunction> logger)
{
_logger = logger;
}
public async Task<CanGoToStationRequestResponse> ExecuteAsync(CanGoToStationRequestRequest request)
{
return new CanGoToStationRequestResponse
{
CanGoToStation = true,
Delta = 0,
TimeStampUpdate = 0,
};
}
}
@@ -0,0 +1,65 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYCancelActiveContractRequest {
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
}
public class FYCancelActiveContractResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
}
[CloudScriptFunction("CancelActiveContract")]
public class CancelActiveContract : ICloudScriptFunction<FYCancelActiveContractRequest, FYCancelActiveContractResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public CancelActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYCancelActiveContractResult> ExecuteAsync(FYCancelActiveContractRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"ContractsActive"});
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var contractActiveIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == request.ContractID);
if (contractActiveIdx == -1) {
return new FYCancelActiveContractResult
{
UserID = userId,
Error = "Failed to find active contract",
};
}
contractsActive.Contracts.RemoveAt(contractActiveIdx);
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["ContractsActive"] = JsonSerializer.Serialize(contractsActive) }
);
return new FYCancelActiveContractResult
{
UserID = userId,
Error = "",
ContractID = request.ContractID,
};
}
}
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CheckAndPerformUserRollbackIfNeededRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class CheckAndPerformUserRollbackIfNeededResponse
{
[JsonPropertyName("result")]
public int Result { get; set; }
}
[CloudScriptFunction("CheckAndPerformUserRollbackIfNeeded")]
public class CheckAndPerformUserRollbackIfNeededFunction : ICloudScriptFunction<CheckAndPerformUserRollbackIfNeededRequest, CheckAndPerformUserRollbackIfNeededResponse>
{
private readonly ILogger<CheckAndPerformUserRollbackIfNeededFunction> _logger;
public CheckAndPerformUserRollbackIfNeededFunction(ILogger<CheckAndPerformUserRollbackIfNeededFunction> logger)
{
_logger = logger;
}
public async Task<CheckAndPerformUserRollbackIfNeededResponse> ExecuteAsync(CheckAndPerformUserRollbackIfNeededRequest request)
{
_logger.LogInformation("Processing user rollback if needed");
return new CheckAndPerformUserRollbackIfNeededResponse
{
Result = 1,
};
}
}
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CheckAndWipeDataForCurrentSeasonRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class CheckAndWipeDataForCurrentSeasonResponse
{
[JsonPropertyName("result")]
public int Result { get; set; }
}
[CloudScriptFunction("CheckAndWipeDataForCurrentSeason")]
public class CheckAndWipeDataForCurrentSeasonFunction : ICloudScriptFunction<CheckAndWipeDataForCurrentSeasonRequest, CheckAndWipeDataForCurrentSeasonResponse>
{
private readonly ILogger<CheckAndWipeDataForCurrentSeasonFunction> _logger;
public CheckAndWipeDataForCurrentSeasonFunction(ILogger<CheckAndWipeDataForCurrentSeasonFunction> logger)
{
_logger = logger;
}
public async Task<CheckAndWipeDataForCurrentSeasonResponse> ExecuteAsync(CheckAndWipeDataForCurrentSeasonRequest request)
{
_logger.LogInformation("Processing check data wipe for current season");
return new CheckAndWipeDataForCurrentSeasonResponse
{
Result = 1,
};
}
}
@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CheckClientVersionUpToDateRequest
{
[JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class CheckClientVersionUpToDateResponse
{
[JsonPropertyName("isClientUpToDate")]
public bool IsClientUpToDate { get; set; }
}
[CloudScriptFunction("CheckClientVersionUpToDate")]
public class CheckClientVersionUpToDateFunction : ICloudScriptFunction<CheckClientVersionUpToDateRequest, CheckClientVersionUpToDateResponse>
{
private readonly ILogger<CheckClientVersionUpToDateFunction> _logger;
public CheckClientVersionUpToDateFunction(ILogger<CheckClientVersionUpToDateFunction> logger)
{
_logger = logger;
}
public async Task<CheckClientVersionUpToDateResponse> ExecuteAsync(CheckClientVersionUpToDateRequest request)
{
_logger.LogInformation("Processing client version check request for client {Version}",
request.Version);
// TODO: Validate login
// if (string.IsNullOrEmpty(request.LoginNonce))
// {
// return new CheckClientVersionUpToDateResponse
// {
// IsClientUpToDate = false,
// };
// }
return new CheckClientVersionUpToDateResponse
{
IsClientUpToDate = true,
};
}
}
@@ -0,0 +1,259 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYClaimCompletedActiveContractRewardsRequest {
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
[JsonPropertyName("contractsToUnlock")]
public string[] ContractsToUnlock { get; set; }
}
public class FYClaimCompletedActiveContractRewardsResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("claimedContractId")]
public string ClaimedContractID { get; set; }
[JsonPropertyName("newContractIdOnBoard")]
public string NewContractIdOnBoard { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("itemsGranted")]
public List<FYCustomItemInfo> ItemsGranted { get; set; }
[JsonPropertyName("itemsUpdatedOrRemoved")]
public List<FYCustomItemInfo> ItemsUpdatedOrRemoved { get; set; }
[JsonPropertyName("playerFactionProgressData")]
public FYPlayerFactionProgressData PlayerFactionProgressData { get; set; }
[JsonPropertyName("contractsActivated")]
public List<FYActiveContractPlayerData> ContractsActivated { get; set; }
[JsonPropertyName("status")]
public int Status { get; set; } // EYClaimContractRewardsStatus
[JsonPropertyName("updatedSeasonXp")]
public int UpdatedSeasonXp { get; set; }
}
public class FYGetCompletedContractsResult {
[JsonPropertyName("contractsIds")]
public List<string> ContractsIDs { get; set; }
};
[CloudScriptFunction("ClaimActiveContract")]
public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveContractRewardsRequest, FYClaimCompletedActiveContractRewardsResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public ClaimActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYClaimCompletedActiveContractRewardsResult> ExecuteAsync(FYClaimCompletedActiveContractRewardsRequest 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", "Blueprints"});
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var contract = contracts[request.ContractID];
if (contract == null) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "Contract not found",
Status = 13, // EYClaimContractRewardsStatus::WRONG_CONTRACT_ID
};
}
var factionKey = "FactionProgression" + contract.Faction;
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey}
);
var factionProgression = JsonSerializer.Deserialize<int>(userData[factionKey].Value);
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var contractsCompleted = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(userData["ContractsOneTimeCompleted"].Value);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var targetContractIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == request.ContractID);
if (targetContractIdx == -1) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "Contract not active",
Status = 8, // EYClaimContractRewardsStatus::NO_ACTIVE_CONTRACT
};
}
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var targetContract = contractsActive.Contracts[targetContractIdx];
List<FYCustomItemInfo> itemsUpdatedOrRemoved = [];
HashSet<string> deletedItemsIds = [];
for (var i = 0; i < contract.Objectives.Length; i++) {
var objective = contract.Objectives[i];
int remaining = objective.MaxProgress;
if (objective.Type == EYContractObjectiveType.OwnNumOfItem) {
// NOTE: The backend may perform item ownership validation
// since it has the information about the player's items.
foreach (var item in inventory) {
if (item.BaseItemId != objective.ItemToOwn) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
}
itemsUpdatedOrRemoved.Add(item);
if (remaining <= 0) {
break;
}
}
} else {
// NOTE: A game server must write the correct progress
// for other types of objectives.
// The objectives are mapped by array index.
remaining -= targetContract.Progress[i];
}
if (remaining > 0) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "Objective not met",
Status = 16, // EYClaimContractRewardsStatus::PROGRESS_MISSING
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
List<FYCustomItemInfo> itemsGranted = new List<FYCustomItemInfo>();
List<FYCurrencyItem> changedCurrencies = [];
foreach (var reward in contract.Rewards) {
if (reward.ItemID == "SoftCurrency") {
balance.SoftCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (reward.ItemID == "Aurum") {
balance.HardCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else if (reward.ItemID == "InsuranceToken") {
balance.InsuranceTokens += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceToken", Amount = balance.InsuranceTokens });
} else {
// TODO: Inventory limit check
var blueprintData = blueprints[reward.ItemID];
var remainingAmount = reward.Amount * blueprintData.AmountPerPurchase;
// Populate incomplete stacks first
foreach (var item in newInventory) {
if (item.BaseItemId != reward.ItemID || item.Amount >= blueprintData.MaxAmountPerStack) {
continue;
}
var remainingSpace = blueprintData.MaxAmountPerStack - item.Amount;
var amountToAdd = Math.Min(remainingAmount, remainingSpace);
item.Amount += amountToAdd;
remainingAmount -= amountToAdd;
itemsUpdatedOrRemoved.Add(item);
if (remainingAmount == 0) {
break;
}
}
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
var itemInfo = new FYCustomItemInfo{
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = reward.ItemID,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
newInventory.Add(itemInfo);
itemsGranted.Add(itemInfo);
remainingAmount -= itemStackAmount;
}
}
}
factionProgression += contract.ReputationIncrease;
contractsActive.Contracts.RemoveAt(targetContractIdx);
// Main contracts can be completed only once.
if (contract.IsMainContract) {
contractsCompleted.ContractsIDs.Add(request.ContractID);
}
var newContracts = new List<FYActiveContractPlayerData>();
foreach (var contractIdToUnlock in request.ContractsToUnlock) {
var contractToUnlock = contracts[contractIdToUnlock];
var progress = new int[contractToUnlock.Objectives.Length];
var newContract = new FYActiveContractPlayerData {
ContractID = contractIdToUnlock,
Progress = progress,
};
contractsActive.Contracts.Add(newContract);
newContracts.Add(newContract);
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["ContractsOneTimeCompleted"] = JsonSerializer.Serialize(contractsCompleted),
["ContractsActive"] = JsonSerializer.Serialize(contractsActive),
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(newInventory),
[factionKey] = JsonSerializer.Serialize(factionProgression),
}
);
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "",
ClaimedContractID = request.ContractID,
ContractsActivated = newContracts,
ItemsUpdatedOrRemoved = itemsUpdatedOrRemoved,
ChangedCurrencies = changedCurrencies.ToArray(),
ItemsGranted = itemsGranted,
NewContractIdOnBoard = "", // TODO: This should be a replacement contract apparently?
PlayerFactionProgressData = new FYPlayerFactionProgressData {
FactionID = contract.Faction,
CurrentProgression = factionProgression,
},
Status = 18, // EYClaimContractRewardsStatus::OK
UpdatedSeasonXp = 0 // TODO: Probably a separate season XP rate configured by server?
};
}
}
@@ -0,0 +1,248 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYClaimGeneratorIncomeClientResult {
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("generatorId")]
public string GeneratorID { get; set; }
[JsonPropertyName("changedCurrenciesBalances")]
public FYCurrencyItem[] ChangedCurrenciesBalances { get; set; }
[JsonPropertyName("grantedItems")]
public List<FYCustomItemInfo> GrantedItems { get; set; }
[JsonPropertyName("lastClaimTime")]
public FYTimestamp LastClaimTime { get; set; }
};
public class FYClaimGeneratorIncomeClientRequest {
[JsonPropertyName("generatorId")]
public string GeneratorID { get; set; }
};
public class FYPassiveGenerator {
[JsonPropertyName("generatorId")]
public string GeneratorID { get; set; }
[JsonPropertyName("lastClaimTime")]
public FYTimestamp LastClaimTime { get; set; }
}
[CloudScriptFunction("ClaimGeneratorIncomeClient")]
public class ClaimGeneratorIncomeClient : ICloudScriptFunction<FYClaimGeneratorIncomeClientRequest, FYClaimGeneratorIncomeClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
static readonly Random random = new();
private TitleDataDailyCrateInfo[] PickItems(TitleDataDailyCrateInfo[] items, int numPicks)
{
var itemList = new (TitleDataDailyCrateInfo item, float cumulativeWeight)[items.Length];
float totalWeight = 0;
for (int i = 0; i < items.Length; i++) {
var item = items[i];
totalWeight += item.Weight;
itemList[i] = (item, totalWeight);
}
var selectedItems = new TitleDataDailyCrateInfo[numPicks];
for (int i = 0; i < numPicks; i++) {
float pick = (float)random.NextDouble() * totalWeight;
int left = 0, right = itemList.Length - 1;
while (left < right) {
int mid = (left + right) / 2;
if (itemList[mid].cumulativeWeight < pick) {
left = mid + 1;
} else {
right = mid;
}
}
selectedItems[i] = itemList[left].item;
}
return selectedItems;
}
public ClaimGeneratorIncomeClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYClaimGeneratorIncomeClientResult> ExecuteAsync(FYClaimGeneratorIncomeClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"Generators__2021_09_09", "CharacterTechTreeBonuses", "Balance", "Inventory"}
);
var userGenerators = JsonSerializer.Deserialize<FYPassiveGenerator[]>(userData["Generators__2021_09_09"].Value);
var generator = Array.Find(userGenerators, item => item.GeneratorID == request.GeneratorID);
if (generator == null) {
return new FYClaimGeneratorIncomeClientResult {
UserId = userId,
Error = "Invalid generator ID",
};
}
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var techTreeBonuses = JsonSerializer.Deserialize<CharacterTechTreeBonuses>(userData["CharacterTechTreeBonuses"].Value);
var titleData = _titleDataService.Find(new List<string>{"PassiveGenerators", "DailyCrateRewardsPools", "Blueprints"});
var passiveGenerators = JsonSerializer.Deserialize<Dictionary<string, TitleDataPassiveGeneratorsInfo>>(titleData["PassiveGenerators"]);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
FYCurrencyItem[] changedCurrencies = [];
List<FYCustomItemInfo> grantedItems = [];
switch (generator.GeneratorID) {
case "playerquarters_gen_kmarks": {
var rate = techTreeBonuses.KmarksRate;
if (rate == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "K-Marks Generator Upgrade is not constructed",
};
}
var generatorData = passiveGenerators[generator.GeneratorID];
var hours = (now - generator.LastClaimTime.Seconds) / (60 * generatorData.BaseGenIntervalMinutes);
var cap = techTreeBonuses.KmarksCap + generatorData.BaseCap;
var claimableAmount = Math.Min(hours * rate, cap);
if (claimableAmount == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Generator is not ready yet",
};
}
balance.SoftCurrency += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
break;
}
case "playerquarters_gen_aurum": {
var rate = techTreeBonuses.AurumRate;
if (rate == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Aurum Generator Upgrade is not constructed",
};
}
var generatorData = passiveGenerators[generator.GeneratorID];
var hours = (now - generator.LastClaimTime.Seconds) / (60 * generatorData.BaseGenIntervalMinutes);
var cap = techTreeBonuses.AurumCap + generatorData.BaseCap;
var claimableAmount = (int)Math.Min(hours * rate, cap);
if (claimableAmount == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Generator is not ready yet",
};
}
balance.HardCurrency += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
break;
}
case "playerquarters_gen_crate": {
var crateTier = techTreeBonuses.CrateTier;
if (crateTier == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Crate Generator Upgrade is not constructed",
};
}
var generatorData = passiveGenerators[generator.GeneratorID];
var nextClaimTime = generator.LastClaimTime.Seconds + 60 * generatorData.BaseGenIntervalMinutes;
if (now < nextClaimTime) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Generator is not ready yet",
};
}
var dailyCrateRewards = JsonSerializer.Deserialize<TitleDataDailyCrateInfo[][]>(titleData["DailyCrateRewardsPools"]);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var rewardsPool = dailyCrateRewards[crateTier - 1];
// TODO: Get rewardGrants from PassiveGenerators_CrateRewards_DT
var items = PickItems(rewardsPool, 5);
// TODO: Stackable? GrantedItems doesn't imply granted OR updated.
foreach (var item in items) {
var itemInfo = blueprints[item.Name];
var grantedItem = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = item.Amount,
BaseItemId = item.Name,
Durability = itemInfo.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(grantedItem);
grantedItems.Add(grantedItem);
}
break;
}
default: {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = $"Unknown generator ID: {request.GeneratorID}",
};
}
}
generator.LastClaimTime.Seconds = now;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
["Generators__2021_09_09"] = JsonSerializer.Serialize(userGenerators),
}
);
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "",
GeneratorID = request.GeneratorID,
ChangedCurrenciesBalances = changedCurrencies,
GrantedItems = grantedItems,
LastClaimTime = generator.LastClaimTime,
};
}
}
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class ClientsideFriendsImportRequest
{
[JsonPropertyName("platform")]
public string Platform { get; set; }
[JsonPropertyName("userIds")]
public string[] UserIDs { get; set; }
}
public class ClientsideFriendsImportResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("ClientsideFriendsImport")]
public class ClientsideFriendsImportFunction : ICloudScriptFunction<ClientsideFriendsImportRequest, ClientsideFriendsImportResponse>
{
private readonly ILogger<ClientsideFriendsImportFunction> _logger;
public ClientsideFriendsImportFunction(ILogger<ClientsideFriendsImportFunction> logger)
{
_logger = logger;
}
public async Task<ClientsideFriendsImportResponse> ExecuteAsync(ClientsideFriendsImportRequest request)
{
_logger.LogInformation("Processing friends import");
return new ClientsideFriendsImportResponse{};
}
}
@@ -1,22 +0,0 @@
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("EnterMatchmaking")]
public class EnterMatchmaking : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchAzureFunctionResult>
{
public Task<FYEnterMatchAzureFunctionResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
return Task.FromResult(new FYEnterMatchAzureFunctionResult
{
Success = true,
Address = "127.0.0.1",
ErrorMessage = "",
Port = 7777,
ShardIndex = 0,
SingleplayerStation = true,
MaintenanceMode = false,
SessionTimeJoinDelay = 0
});
}
}
@@ -0,0 +1,46 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SignalR;
using Prospect.Server.Api.Hubs;
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class OnSquadMatchmakingSuccessMessage {
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
}
[CloudScriptFunction("EnterMatchmakingMatch")]
public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
{
private readonly IHubContext<CycleHub> _hubContext;
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext)
{
_hubContext = hubContext;
}
public async Task<FYEnterMatchmakingResult> 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 FYEnterMatchmakingResult
{
Success = true,
ErrorMessage = "",
SingleplayerStation = false,
NumAttempts = 1,
Blocker = 0,
IsMatchTravel = true,
SessionId = "", // TODO: Not sure how this affects match travel
};
}
}
@@ -0,0 +1,18 @@
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("EnterMatchmakingStation")]
public class EnterMatchmakingStationFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
{
public Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
return Task.FromResult(new FYEnterMatchmakingResult
{
Success = true,
ErrorMessage = "",
SingleplayerStation = true, // NOTE: This will always travel to single player station
IsMatchTravel = false,
});
}
}
@@ -0,0 +1,34 @@
using Prospect.Server.Api.Services.CloudScript;
public class FetchSteamEntitlementsRequest
{
// Empty request
}
public class FetchSteamEntitlementsResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("FetchSteamEntitlements")]
public class FetchSteamEntitlementsFunction : ICloudScriptFunction<FetchSteamEntitlementsRequest, FetchSteamEntitlementsResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public FetchSteamEntitlementsFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<FetchSteamEntitlementsResponse> ExecuteAsync(FetchSteamEntitlementsRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
return new FetchSteamEntitlementsResponse
{};
}
}
@@ -0,0 +1,52 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class FYActiveGlobalVanity {
[JsonPropertyName("activeGlobalVanityIds")]
public string[] ActiveGlobalVanityIds { get; set; }
[JsonPropertyName("droppodId")]
public string DropPodID { get; set; }
}
public class FYGetActiveGlobalVanityRequest
{
// Empty request
}
public class FYGetActiveGlobalVanityResponse
{
[JsonPropertyName("activeVanity")]
public FYActiveGlobalVanity ActiveVanity { get; set; }
}
[CloudScriptFunction("GetActiveGlobalVanity")]
public class GetActiveGlobalVanityFunction : ICloudScriptFunction<FYGetActiveGlobalVanityRequest, FYGetActiveGlobalVanityResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetActiveGlobalVanityFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYGetActiveGlobalVanityResponse> ExecuteAsync(FYGetActiveGlobalVanityRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"GlobalVanity"});
var activeVanity = JsonSerializer.Deserialize<FYActiveGlobalVanity>(userData["GlobalVanity"].Value);
return new FYGetActiveGlobalVanityResponse{
ActiveVanity = activeVanity
};
}
}
@@ -1,67 +1,75 @@
using Prospect.Server.Api.Services.Auth.Extensions; using System.Text.Json;
using System.Text.Json.Serialization;
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;
public class FYGetCharacterVanityResponse {
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("returnVanity")]
public FYCharacterVanity ReturnVanity { get; set; }
}
public class FYVanityMaterialItem {
[JsonPropertyName("id")]
public string ID { get; set; }
[JsonPropertyName("materialIndex")]
public int MaterialIndex { get; set; }
}
public class FYCharacterVanity {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("head_item")]
public FYVanityMaterialItem HeadItem { get; set; }
[JsonPropertyName("boots_item")]
public FYVanityMaterialItem BootsItem { get; set; }
[JsonPropertyName("chest_item")]
public FYVanityMaterialItem ChestItem { get; set; }
[JsonPropertyName("glove_item")]
public FYVanityMaterialItem GloveItem { get; set; }
[JsonPropertyName("base_suit_item")]
public FYVanityMaterialItem BaseSuitItem { get; set; }
[JsonPropertyName("melee_weapon_item")]
public FYVanityMaterialItem MeleeWeaponItem { get; set; }
[JsonPropertyName("body_type")]
public int BodyType { get; set; }
[JsonPropertyName("archetype_id")]
public string ArchetypeID { get; set; }
[JsonPropertyName("slot_index")]
public int SlotIndex { get; set; }
}
[CloudScriptFunction("GetCharacterVanity")] [CloudScriptFunction("GetCharacterVanity")]
public class GetCharacterVanity : ICloudScriptFunction<FYGetCharacterVanityRequest, object?> public class GetCharacterVanity : ICloudScriptFunction<FYGetCharacterVanityRequest, FYGetCharacterVanityResponse?>
{ {
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetCharacterVanity(IHttpContextAccessor httpContextAccessor) public GetCharacterVanity(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{ {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
} }
public Task<object?> ExecuteAsync(FYGetCharacterVanityRequest request) public async Task<FYGetCharacterVanityResponse?> ExecuteAsync(FYGetCharacterVanityRequest request)
{ {
var context = _httpContextAccessor.HttpContext; var context = _httpContextAccessor.HttpContext;
if (context == null) if (context == null)
{ {
return Task.FromResult<object?>(null); return null;
} }
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"CharacterVanity"});
var returnVanity = JsonSerializer.Deserialize<FYCharacterVanity>(userData["CharacterVanity"].Value);
return Task.FromResult<object?>(new return new FYGetCharacterVanityResponse{
{ Success = true,
userId = context.User.FindAuthUserId(), ReturnVanity = returnVanity,
error = (object?)null, };
returnVanity = new
{
userId = context.User.FindAuthUserId(),
head_item = new
{
id = "Black02M_Head1",
materialIndex = 0,
},
boots_item = new
{
id = "StarterOutfit01_Boots_M",
materialIndex = 0,
},
chest_item = new
{
id = "StarterOutfit01_Chest_M",
materialIndex = 0,
},
glove_item = new
{
id = "StarterOutfit01_Gloves_M",
materialIndex = 0,
},
base_suit_item = new
{
id = "StarterOutfit01M_BaseSuit",
materialIndex = 0,
},
melee_weapon_item = new
{
id = "Melee_Omega",
materialIndex = 0,
},
body_type = 1,
archetype_id = "TheProspector",
slot_index = 0
}
});
} }
} }
@@ -26,16 +26,7 @@ public class GetCraftingInProgressData : ICloudScriptFunction<FYGetCraftingInPro
{ {
UserId = context.User.FindAuthUserId(), UserId = context.User.FindAuthUserId(),
Error = string.Empty, Error = string.Empty,
ItemCurrentlyBeingCrafted = new FYItemCurrentlyBeingCrafted ItemCurrentlyBeingCrafted = {},
{
ItemId = null,
ItemRarity = -1,
PurchaseAmount = -1,
UtcTimestampWhenCraftingStarted = new FYTimestamp
{
Seconds = 0
}
}
}); });
} }
} }
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class GetLoginQueuePositionRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; }
}
public class GetLoginQueuePositionResponse
{
[JsonPropertyName("waitingUserCount")]
public int WaitingUserCount { get; set; }
}
[CloudScriptFunction("GetLoginQueuePosition")]
public class GetLoginQueuePositionFunction : ICloudScriptFunction<GetLoginQueuePositionRequest, GetLoginQueuePositionResponse>
{
private readonly ILogger<GetLoginQueuePositionFunction> _logger;
public GetLoginQueuePositionFunction(ILogger<GetLoginQueuePositionFunction> logger)
{
_logger = logger;
}
public async Task<GetLoginQueuePositionResponse> ExecuteAsync(GetLoginQueuePositionRequest request)
{
_logger.LogInformation("Processing user rollback if needed");
return new GetLoginQueuePositionResponse
{
WaitingUserCount = 0,
};
}
}
@@ -0,0 +1,77 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
public class GetPlayerQuartersDataClientRequest
{
// Empty request
}
public class FYGetPlayerQuartersDataClientResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("level")]
public int Level { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
}
public class FYPlayerQuarterStatus
{
[JsonPropertyName("level")]
public int Level { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
}
[CloudScriptFunction("GetPlayerQuartersDataClient")]
public class GetPlayerQuartersDataClientFunction : ICloudScriptFunction<GetPlayerQuartersDataClientRequest, FYGetPlayerQuartersDataClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public GetPlayerQuartersDataClientFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYGetPlayerQuartersDataClientResponse> ExecuteAsync(GetPlayerQuartersDataClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"PlayerQuartersLevel"}
);
var titleData = _titleDataService.Find(new List<string>{ "PlayerQuarters" });
var playerQuartersData = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
var userPlayerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var remainingTime = 0;
if (userPlayerQuarters.UpgradeStartedTime.Seconds != 0) {
var nextLevel = playerQuartersData[userPlayerQuarters.Level - 1];
remainingTime = userPlayerQuarters.UpgradeStartedTime.Seconds + nextLevel.UpgradeSeconds - now;
}
return new FYGetPlayerQuartersDataClientResponse {
UserID = userId,
Level = userPlayerQuarters.Level,
RemainingTimeInSeconds = remainingTime,
UpgradeStartedTime = userPlayerQuarters.UpgradeStartedTime,
};
}
}
@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
// ["DropsTotal","DeathsTotal","DeathsPlayerTotal","DeathsCreaturesTotal","DeathsOthersTotal","EvacsTotal","DeathsConsecutive","EvacsConsecutive","KillsPlayersTotal","KillsPlayersMax","KillsCreaturesTotal","KillsCreaturesMax","DamagePlayersTotal","DamagePlayersMax","DamageCreaturesTotal","DamageCreaturesMax","ContractsCompletedKorolevTotal","ContractsCompletedOsirisTotal","ContractsCompletedICATotal","ContractsCompletedTotal","MatchDurationMax","MatchDurationAvg","KMarksMatchMax","KMarksMatchTotal","KMarksTotal"]
public class GetPlayerStatisticsRequestsClientRequest
{
[JsonPropertyName("statistics")]
public string[] Statistics { get; set; }
}
public class GetPlayerStatisticsRequestsClientResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("GetPlayerStatisticsRequestsClient")]
public class GetPlayerStatisticsRequestsClientFunction : ICloudScriptFunction<GetPlayerStatisticsRequestsClientRequest, GetPlayerStatisticsRequestsClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public GetPlayerStatisticsRequestsClientFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<GetPlayerStatisticsRequestsClientResponse> ExecuteAsync(GetPlayerStatisticsRequestsClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
return new GetPlayerStatisticsRequestsClientResponse
{
};
}
}
@@ -9,7 +9,7 @@ public class GetSignalRConnection : ICloudScriptFunction<FYGetSignalRConnection,
{ {
return Task.FromResult(new FYGetSignalRConnectionResult return Task.FromResult(new FYGetSignalRConnectionResult
{ {
Url = "https://localhost:5443/signalr/?hub=pubsub", Url = "https://2ea46.playfabapi.com/signalr/?hub=pubsub",
AccessToken = "TEST" AccessToken = "TEST"
}); });
} }
@@ -0,0 +1,69 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Functions;
using Prospect.Server.Api.Services.UserData;
public class GetTechTreeNodeDataClientRequest
{
// Empty request
}
public class GetTechTreeNodeDataClientResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("nodesData")]
public FYTechTreeNodeStatus[] NodesData { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
}
[CloudScriptFunction("GetTechTreeNodeDataClient")]
public class GetTechTreeNodeDataClientFunction : ICloudScriptFunction<GetTechTreeNodeDataClientRequest, GetTechTreeNodeDataClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public GetTechTreeNodeDataClientFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<GetTechTreeNodeDataClientResponse> ExecuteAsync(GetTechTreeNodeDataClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"TechTreeNodeData"}
);
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes"});
var techTreeNodes = JsonSerializer.Deserialize<Dictionary<string, TitleDataTechTreeNodeInfo>>(titleData["TechTreeNodes"]);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var remainingTime = 0;
if (userTechTree.NodeInProgress != "") {
var nodeInProgress = userTechTree.Nodes[userTechTree.NodeInProgress];
var node = techTreeNodes[userTechTree.NodeInProgress];
remainingTime = nodeInProgress.UpgradeStartedTime.Seconds + node.PerkLevels[nodeInProgress.Level].UpgradeSeconds - now;
}
return new GetTechTreeNodeDataClientResponse
{
UserID = userId,
NodesData = userTechTree.Nodes.Values.ToArray(),
RemainingTimeInSeconds = remainingTime,
};
}
}
@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class YOnLoginRequest
{
[JsonPropertyName("gamePlatform")]
public string GamePlatform { get; set; }
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
[JsonPropertyName("customTags")]
public Dictionary<string, string> CustomTags { get; set; } = new();
}
public class OnLoginResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("OnLogin")]
public class OnLoginFunction : ICloudScriptFunction<YOnLoginRequest, OnLoginResponse>
{
private readonly ILogger<OnLoginFunction> _logger;
public OnLoginFunction(ILogger<OnLoginFunction> logger)
{
_logger = logger;
}
public async Task<OnLoginResponse> ExecuteAsync(YOnLoginRequest request)
{
_logger.LogInformation("Processing login request for platform {Platform} with nonce {Nonce}",
request.GamePlatform, request.LoginNonce);
// Example: Validate login
// if (string.IsNullOrEmpty(request.LoginNonce))
// {
// return new OnLoginResponse
// {};
// }
// Simulating user authentication logic
var sessionToken = Guid.NewGuid().ToString();
return new OnLoginResponse
{};
}
}
@@ -0,0 +1,99 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYPlayerQuarterUpgradePurchaseClientRequest {
// Empty request
}
public class FYPlayerQuarterUpgradePurchaseClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("newLevel")]
public int NewLevel { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public string[] DeletedItemsIds { get; set; }
}
[CloudScriptFunction("PlayerQuarterUpgradePurchaseClient")]
public class PlayerQuarterUpgradePurchaseClient : ICloudScriptFunction<FYPlayerQuarterUpgradePurchaseClientRequest, FYPlayerQuarterUpgradePurchaseClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public PlayerQuarterUpgradePurchaseClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYPlayerQuarterUpgradePurchaseClientResult> ExecuteAsync(FYPlayerQuarterUpgradePurchaseClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"PlayerQuartersLevel"}
);
var userPlayerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
var titleData = _titleDataService.Find(new List<string>{"PlayerQuarters"});
var playerQuarters = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var endTime = playerQuarters[userPlayerQuarters.Level - 1].UpgradeSeconds + userPlayerQuarters.UpgradeStartedTime.Seconds;
if (now < endTime) {
return new FYPlayerQuarterUpgradePurchaseClientResult
{
UserID = userId,
Error = "Upgrade is in progress",
};
}
var upgradeStartedTime = new FYTimestamp {
Seconds = 0,
};
userPlayerQuarters.Level++;
userPlayerQuarters.UpgradeStartedTime = upgradeStartedTime;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["PlayerQuartersLevel"] = JsonSerializer.Serialize(userPlayerQuarters),
}
);
return new FYPlayerQuarterUpgradePurchaseClientResult
{
UserID = userId,
Error = "",
NewLevel = userPlayerQuarters.Level,
UpgradeStartedTime = upgradeStartedTime,
RemainingTimeInSeconds = 0,
ChangedItems = [],
DeletedItemsIds = [],
ChangedCurrencies = [],
};
}
}
@@ -0,0 +1,273 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class PurchaseWeaponShopItemRequest
{
[JsonPropertyName("blueprintName")]
public string BlueprintName { get; set; }
[JsonPropertyName("blueprintRarity")]
public int BlueprintRarity { get; set; }
[JsonPropertyName("purchaseAmount")]
public int PurchaseAmount { get; set; }
[JsonPropertyName("baseItemId")]
public string BaseItemID { get; set; }
[JsonPropertyName("shopItemBelongsTo")]
public string ShopItemBelongsTo { get; set; }
}
public class PurchaseWeaponShopItemResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("itemRarity")]
public int ItemRarity { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("itemsGrantedOrUpdated")]
public List<FYCustomItemInfo> ItemsGrantedOrUpdated { get; set; }
[JsonPropertyName("deletedItemsIds")]
public HashSet<string> DeletedItemsIds { get; set; }
[JsonPropertyName("purchaseAmount")]
public int PurchaseAmount { get; set; }
[JsonPropertyName("blueprintName")]
public string BlueprintName { get; set; }
[JsonPropertyName("shopItemBelongsTo")]
public string ShopItemBelongsTo { get; set; }
}
public class FYCurrencyItem {
[JsonPropertyName("currencyName")]
public string CurrencyName { get; set; }
[JsonPropertyName("amount")]
public int Amount { get; set; }
}
public class FYModItems {
[JsonPropertyName("m")]
public int[] M { get; set; } // mods
};
// FYItemOriginData
public class FYItemOriginBackend {
[JsonPropertyName("t")]
public string T { get; set; } // type
[JsonPropertyName("p")]
public string P { get; set; } // playfabid
[JsonPropertyName("g")]
public string G { get; set; } // guid
};
// FYRolledPerkEntry
public class FYRolledPerkBackend {
[JsonPropertyName("i")]
public int I { get; set; } // short perk id
[JsonPropertyName("r")]
public float R { get; set; } // rolled range value
};
public class FYCustomItemInfo {
[JsonPropertyName("itemId")]
public string ItemId { get; set; }
[JsonPropertyName("baseItemId")]
public string BaseItemId { get; set; }
[JsonPropertyName("primaryVanityId")] // Item skin
public int PrimaryVanityId { get; set; }
[JsonPropertyName("secondaryVanityId")] // Item charm
public int SecondaryVanityId { get; set; }
[JsonPropertyName("amount")]
public int Amount { get; set; }
[JsonPropertyName("durability")]
public int Durability { get; set; }
[JsonPropertyName("modData")]
public FYModItems ModData { get; set; }
[JsonPropertyName("rolledPerks")]
public FYRolledPerkBackend[] RolledPerks { get; set; }
[JsonPropertyName("insurance")]
public string Insurance { get; set; }
[JsonPropertyName("insuranceOwnerPlayfabId")]
public string InsuranceOwnerPlayfabId { get; set; }
[JsonPropertyName("insuredAttachmentId")]
public string InsuredAttachmentId { get; set; }
[JsonPropertyName("origin")]
public FYItemOriginBackend Origin { get; set; }
};
public class CustomVanityItem {
[JsonPropertyName("baseItemId")]
public string BaseItemId { get; set; }
[JsonPropertyName("origin")]
public FYItemOriginBackend Origin { get; set; }
};
[CloudScriptFunction("PurchaseWeaponShopItem")]
public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeaponShopItemRequest, PurchaseWeaponShopItemResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<PurchaseWeaponShopItemFunction> _logger;
public PurchaseWeaponShopItemFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger<PurchaseWeaponShopItemFunction> logger)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<PurchaseWeaponShopItemResponse> ExecuteAsync(PurchaseWeaponShopItemRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
if (request.PurchaseAmount <= 0) {
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Invalid purchase amount",
};
}
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance"});
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var blueprintsData = _titleDataService.Find(new List<string>{"Blueprints"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(blueprintsData["Blueprints"]);
if (!blueprints.ContainsKey(request.BaseItemID)) {
_logger.LogError("Failed to find item ID {ItemID}", request.BaseItemID);
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Invalid base item ID",
};
}
var blueprintData = blueprints[request.BaseItemID];
// TODO: Contract unlock criteria
// Validate price
var shopCraftingData = blueprintData.ItemShopsCraftingData[request.ShopItemBelongsTo];
// TODO: Refactor
List<FYCustomItemInfo> grantedOrUpdated = [];
HashSet<string> deletedItemsIds = [];
List<FYCurrencyItem> changedCurrencies = [];
foreach (var ingredient in shopCraftingData.ItemRecipeIngredients) {
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
var buyCost = request.PurchaseAmount * ingredient.Amount;
remaining -= buyCost;
balance.SoftCurrency -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (ingredient.Currency == "Aurum") {
var buyCost = request.PurchaseAmount * ingredient.Amount;
remaining -= buyCost;
balance.HardCurrency -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else {
foreach (var item in inventory) {
if (item.BaseItemId != ingredient.Currency) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
} else {
grantedOrUpdated.Add(item);
}
if (remaining <= 0) {
break;
}
}
}
if (remaining > 0) {
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Missing required items",
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
// TODO: Inventory limit check
// TODO: Refactor into a helper function
var remainingAmount = request.PurchaseAmount * blueprintData.AmountPerPurchase;
// Populate incomplete stacks first
foreach (var item in newInventory) {
if (item.BaseItemId != request.BaseItemID || item.Amount >= blueprintData.MaxAmountPerStack) {
continue;
}
var remainingSpace = blueprintData.MaxAmountPerStack - item.Amount;
var amountToAdd = Math.Min(remainingAmount, remainingSpace);
item.Amount += amountToAdd;
remainingAmount -= amountToAdd;
grantedOrUpdated.Add(item);
if (remainingAmount == 0) {
break;
}
}
// Then create new items
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
var grantedItem = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = request.BaseItemID,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
newInventory.Add(grantedItem);
grantedOrUpdated.Add(grantedItem);
remainingAmount -= itemStackAmount;
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(newInventory),
["Balance"] = JsonSerializer.Serialize(balance),
}
);
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "",
BlueprintName = request.BlueprintName,
ItemRarity = request.BlueprintRarity,
PurchaseAmount = request.PurchaseAmount,
ChangedCurrencies = changedCurrencies.ToArray(),
DeletedItemsIds = deletedItemsIds,
ItemsGrantedOrUpdated = grantedOrUpdated,
ShopItemBelongsTo = request.ShopItemBelongsTo
};
}
}
@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RefreshVictimCompensationPackagesRequest
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
}
public class RefreshVictimCompensationPackagesResponse
{
[JsonPropertyName("compensationPackages")]
public object[] CompensationPackages { get; set; }
}
[CloudScriptFunction("RefreshVictimCompensationPackages")]
public class RefreshVictimCompensationPackagesFunction : ICloudScriptFunction<RefreshVictimCompensationPackagesRequest, RefreshVictimCompensationPackagesResponse>
{
private readonly ILogger<RefreshVictimCompensationPackagesFunction> _logger;
public RefreshVictimCompensationPackagesFunction(ILogger<RefreshVictimCompensationPackagesFunction> logger)
{
_logger = logger;
}
public async Task<RefreshVictimCompensationPackagesResponse> ExecuteAsync(RefreshVictimCompensationPackagesRequest request)
{
_logger.LogInformation("Processing refresh victim compensation packages");
return new RefreshVictimCompensationPackagesResponse
{
CompensationPackages = [],
};
}
}
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RegisterUserMacAddressRequestRequest
{
[JsonPropertyName("macAddress")]
public string MacAddress { get; set; }
}
public class RegisterUserMacAddressRequestResponse
{
[JsonPropertyName("result")]
public bool Result { get; set; }
}
[CloudScriptFunction("RegisterUserMacAddressRequest")]
public class RegisterUserMacAddressRequestFunction : ICloudScriptFunction<RegisterUserMacAddressRequestRequest, RegisterUserMacAddressRequestResponse>
{
private readonly ILogger<RegisterUserMacAddressRequestFunction> _logger;
public RegisterUserMacAddressRequestFunction(ILogger<RegisterUserMacAddressRequestFunction> logger)
{
_logger = logger;
}
public async Task<RegisterUserMacAddressRequestResponse> ExecuteAsync(RegisterUserMacAddressRequestRequest request)
{
return new RegisterUserMacAddressRequestResponse
{
Result = true,
};
}
}
@@ -0,0 +1,103 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYRepairItemRequest {
[JsonPropertyName("instanceId")]
public string InstanceID { get; set; }
}
public class FYRepairItemResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
}
[CloudScriptFunction("RepairItem")]
public class RepairItem : ICloudScriptFunction<FYRepairItemRequest, FYRepairItemResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public RepairItem(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYRepairItemResult> ExecuteAsync(FYRepairItemRequest 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>{"Blueprints"});
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"Balance", "Inventory"}
);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var item = inventory.Find(item => item.ItemId == request.InstanceID);
if (item == null) {
return new FYRepairItemResult
{
UserID = userId,
Error = "Item not found",
};
}
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var blueprintData = blueprints[item.BaseItemId];
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var repairCost = blueprintData.RepairCostBase + (int)Math.Ceiling((1 - (float)item.Durability / blueprintData.DurabilityMax) * blueprintData.RepairCostMaxDurability);
if (item.Durability == 0) {
repairCost += blueprintData.RepairCostModifierBroken;
}
if (balance.SoftCurrency < repairCost) {
return new FYRepairItemResult
{
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= repairCost;
item.Durability = blueprintData.DurabilityMax;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
}
);
return new FYRepairItemResult
{
UserID = userId,
Error = "",
ChangedItems = [item],
ChangedCurrencies = [
new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency },
],
};
}
}
@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class ReportBiEventsRequest
{
[JsonPropertyName("messages")]
public object[] Messages { get; set; }
}
public class ReportBiEventsResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("ReportBiEvents")]
public class ReportBiEventsFunction : ICloudScriptFunction<ReportBiEventsRequest, ReportBiEventsResponse>
{
private readonly ILogger<ReportBiEventsFunction> _logger;
public ReportBiEventsFunction(ILogger<ReportBiEventsFunction> logger)
{
_logger = logger;
}
public async Task<ReportBiEventsResponse> ExecuteAsync(ReportBiEventsRequest request)
{
_logger.LogInformation("Processing Bi Event");
return new ReportBiEventsResponse{};
}
}
@@ -0,0 +1,109 @@
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("RequestActiveObjectivesAndBoardsData")]
public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQueryFactionProgressionRequest, FYGetPlayerContractsResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestActiveObjectivesAndBoardsData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYQueryFactionProgressionRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new FYGetPlayerContractsResult{};
}
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
return new FYGetPlayerContractsResult
{
UserId = userId,
Error = null,
ActiveContracts = contractsActive.Contracts,
FactionsContracts = new FYFactionsContractsData
{
Boards = new List<FYFactionContractsData>
{
new FYFactionContractsData
{
FactionId = "ICA",
Contracts = new List<FYFactionContractData>
{
new FYFactionContractData
{
ContractId = "NEW-Easy-ICA-Gather-1",
},
new FYFactionContractData
{
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
};
}
}
@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestCanReconnectRequest
{
// Empty request
}
public class RequestCanReconnectResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
//[JsonPropertyName("error")]
//public int Error { get; set; }
//[JsonPropertyName("changedItems")]
//public int changedItems { get; set; }
//[JsonPropertyName("sessionId")]
//public int SessionID { get; set; }
//[JsonPropertyName("info")]
//public int Info { get; set; }
//[JsonPropertyName("lastSessionResult")]
//public int LastSessionResult { get; set; }
//[JsonPropertyName("lastSessionId")]
//public int LastSessionID { get; set; }
}
[CloudScriptFunction("RequestCanReconnect")]
public class RequestCanReconnectFunction : ICloudScriptFunction<RequestCanReconnectRequest, RequestCanReconnectResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public RequestCanReconnectFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestCanReconnectResponse> ExecuteAsync(RequestCanReconnectRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new RequestCanReconnectResponse
{};
}
return new RequestCanReconnectResponse
{
//Error = 0,
//Info = 0,
//LastSessionResult = 0,
//LastSessionID = 0,
//SessionID = 0,
//changedItems = 0,
UserID = context.User.FindAuthUserId(),
};
}
}
@@ -0,0 +1,76 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class FYFortunaPassClaimedRewards {
[JsonPropertyName("rewardsIds")]
public List<string> RewardsIDs { get; set; }
};
public class RequestClaimFortunaPassRewardsRequest
{
[JsonPropertyName("rewardsIds")]
public string[] RewardsIDs { get; set; }
}
public class RequestClaimFortunaPassRewardsResponse
{
[JsonPropertyName("userIds")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("errorType")]
public int ErrorType { get; set; }
[JsonPropertyName("grantedItems")]
public FYCustomItemInfo[] GrantedItems { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
}
[CloudScriptFunction("RequestClaimFortunaPassRewards")]
public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction<RequestClaimFortunaPassRewardsRequest, RequestClaimFortunaPassRewardsResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestClaimFortunaPassRewardsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<RequestClaimFortunaPassRewardsResponse> ExecuteAsync(RequestClaimFortunaPassRewardsRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"FortunaPass2_ClaimedRewards"});
var claimedRewards = JsonSerializer.Deserialize<FYFortunaPassClaimedRewards>(userData["FortunaPass2_ClaimedRewards"].Value);
foreach (var rewardId in request.RewardsIDs) {
claimedRewards.RewardsIDs.Add(rewardId);
}
var output = JsonSerializer.Serialize(claimedRewards);
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["FortunaPass2_ClaimedRewards"] = output,
["FortunaPass3_ClaimedRewards"] = output, // TODO: Separate logic for S2 and S3
}
);
return new RequestClaimFortunaPassRewardsResponse
{
UserId = userId,
ErrorType = 3, // EYFortunaPassToastReponseType::Ok
Error = "",
GrantedItems = [],
ChangedCurrencies = [],
};
}
}
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
// Season 1 request
public class RequestIsPlayerStillOnBattleServerRequest
{
}
public class RequestIsPlayerStillOnBattleServerResponse
{
[JsonPropertyName("isStillOnBattleServer")]
public bool IsStillOnBattleServer { get; set; }
[JsonPropertyName("infoStillOnBattleServer")]
public string InfoStillOnBattleServer { get; set; }
}
[CloudScriptFunction("RequestIsPlayerStillOnBattleServer")]
public class RequestIsPlayerStillOnBattleServerFunction : ICloudScriptFunction<RequestIsPlayerStillOnBattleServerRequest, RequestIsPlayerStillOnBattleServerResponse>
{
private readonly ILogger<RequestIsPlayerStillOnBattleServerFunction> _logger;
public RequestIsPlayerStillOnBattleServerFunction(ILogger<RequestIsPlayerStillOnBattleServerFunction> logger)
{
_logger = logger;
}
public async Task<RequestIsPlayerStillOnBattleServerResponse> ExecuteAsync(RequestIsPlayerStillOnBattleServerRequest request)
{
return new RequestIsPlayerStillOnBattleServerResponse
{
IsStillOnBattleServer = false,
InfoStillOnBattleServer = "",
};
}
}
@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RequestMigrateSteamAccountFromCB1Request
{
[JsonPropertyName("steamId")]
public string SteamID { get; set; }
}
public class RequestMigrateSteamAccountFromCB1Response
{}
[CloudScriptFunction("RequestMigrateSteamAccountFromCB1")]
public class RequestMigrateSteamAccountFromCB1Function : ICloudScriptFunction<RequestMigrateSteamAccountFromCB1Request, RequestMigrateSteamAccountFromCB1Response>
{
private readonly ILogger<RequestMigrateSteamAccountFromCB1Function> _logger;
public RequestMigrateSteamAccountFromCB1Function(ILogger<RequestMigrateSteamAccountFromCB1Function> logger)
{
_logger = logger;
}
public async Task<RequestMigrateSteamAccountFromCB1Response> ExecuteAsync(RequestMigrateSteamAccountFromCB1Request request)
{
_logger.LogInformation("Processing CB1 account migration request request for Steam account {SteamID}",
request.SteamID);
return new RequestMigrateSteamAccountFromCB1Response
{};
}
}
@@ -0,0 +1,127 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYRequestMissionCompletedRequest {
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
}
public class FYRequestMissionCompletedResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("rewards")]
public List<FYCustomItemInfo> Rewards { get; set; }
[JsonPropertyName("updatedCurrencies")]
public FYCurrencyItem[] UpdatedCurrencies { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
}
[CloudScriptFunction("RequestMissionCompleted")]
public class RequestMissionCompleted : ICloudScriptFunction<FYRequestMissionCompletedRequest, FYRequestMissionCompletedResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public RequestMissionCompleted(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYRequestMissionCompletedResult> ExecuteAsync(FYRequestMissionCompletedRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"OnboardingProgression", "Inventory", "Balance"});
var onboardingMission = JsonSerializer.Deserialize<OnboardingMission>(userData["OnboardingProgression"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var titleData = _titleDataService.Find(new List<string>{"OnboardingMissions", "OnboardingRewardMissions", "Blueprints"});
var rewards = JsonSerializer.Deserialize<Dictionary<string, TitleDataMissionRewardInfo>>(titleData["OnboardingRewardMissions"]);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var missions = JsonSerializer.Deserialize<Dictionary<string, TitleDataMissionInfo>>(titleData["OnboardingMissions"]);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var currentMission = missions[onboardingMission.CurrentMissionID];
var missionRewardId = currentMission.OnboardingRewards.RowName;
var nextMissionId = currentMission.NextMissionRowHandle.RowName;
onboardingMission.CurrentMissionID = nextMissionId;
onboardingMission.Progress = 0;
List<FYCustomItemInfo> rewardedItems = new List<FYCustomItemInfo>();
List<FYCurrencyItem> changedCurrencies = [];
if (rewards.ContainsKey(missionRewardId)) {
var missionRewards = rewards[missionRewardId].RewardEntries;
foreach (var reward in missionRewards) {
if (reward.RewardRowHandle.RowName == "SoftCurrency") {
balance.SoftCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (reward.RewardRowHandle.RowName == "Aurum") {
balance.HardCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else if (reward.RewardRowHandle.RowName == "InsuranceToken") {
balance.InsuranceTokens += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceToken", Amount = balance.InsuranceTokens });
} else {
// TODO: Inventory limit check
var blueprintData = blueprints[reward.RewardRowHandle.RowName];
var remainingAmount = reward.RewardAmount * blueprintData.AmountPerPurchase;
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
// TODO: Mission rewards are probably already given in stacks
var itemInfo = new FYCustomItemInfo{
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = reward.RewardRowHandle.RowName,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(itemInfo);
rewardedItems.Add(itemInfo);
remainingAmount -= itemStackAmount;
}
}
}
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["OnboardingProgression"] = JsonSerializer.Serialize(onboardingMission),
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
}
);
return new FYRequestMissionCompletedResult
{
UserID = userId,
CurrentMissionID = nextMissionId,
Rewards = rewardedItems,
UpdatedCurrencies = changedCurrencies.ToArray(),
Progress = 0
};
}
}
@@ -0,0 +1,83 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYRequestMissionUpdateRequest {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("currentStepName")]
public string CurrentStepName { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
[JsonPropertyName("currentRoomId")]
public string CurrentRoomID { get; set; }
}
public class FYRequestMissionUpdateResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("rewards")]
public object[] Rewards { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
}
public class OnboardingMission {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("currentMissionID")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
[JsonPropertyName("showPopup")]
public bool ShowPopup { get; set; }
}
[CloudScriptFunction("RequestMissionUpdate")]
public class RequestMissionUpdate : ICloudScriptFunction<FYRequestMissionUpdateRequest, FYRequestMissionUpdateResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestMissionUpdate(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYRequestMissionUpdateResult> ExecuteAsync(FYRequestMissionUpdateRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"OnboardingProgression"});
var onboardingMission = JsonSerializer.Deserialize<OnboardingMission>(userData["OnboardingProgression"].Value);
onboardingMission.CurrentMissionID = request.CurrentMissionID;
onboardingMission.Progress = request.Progress;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["OnboardingProgression"] = JsonSerializer.Serialize(onboardingMission) }
);
return new FYRequestMissionUpdateResult
{
UserID = userId,
Error = "",
CurrentMissionID = request.CurrentMissionID,
Rewards = [],
Progress = request.Progress
};
}
}
@@ -0,0 +1,50 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RequestNewLegalAgreementsRequest
{
[JsonPropertyName("gamePlatform")]
public string GamePlatform { get; set; }
[JsonPropertyName("nDAVersion")]
public int NDAVersion { get; set; }
[JsonPropertyName("eULAVersion")]
public int EULAVersion { get; set; }
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class RequestNewLegalAgreementsResponse
{
[JsonPropertyName("requiredNDAVersion")]
public int NDAVersion { get; set; }
[JsonPropertyName("requiredEULAVersion")]
public int EULAVersion { get; set; }
[JsonPropertyName("hasAccepted")]
public int HasAccepted { get; set; }
}
[CloudScriptFunction("RequestNewLegalAgreements")]
public class RequestNewLegalAgreementsFunction : ICloudScriptFunction<RequestNewLegalAgreementsRequest, RequestNewLegalAgreementsResponse>
{
private readonly ILogger<RequestNewLegalAgreementsFunction> _logger;
public RequestNewLegalAgreementsFunction(ILogger<RequestNewLegalAgreementsFunction> logger)
{
_logger = logger;
}
public async Task<RequestNewLegalAgreementsResponse> ExecuteAsync(RequestNewLegalAgreementsRequest request)
{
_logger.LogInformation("Processing request new legal agreements for platform {Platform} with nonce {Nonce}",
request.GamePlatform, request.LoginNonce);
return new RequestNewLegalAgreementsResponse
{
NDAVersion = request.NDAVersion,
EULAVersion = request.EULAVersion,
HasAccepted = 1,
};
}
}
@@ -39,17 +39,14 @@ public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsR
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Easy-ICA-Gather-1", ContractId = "NEW-Easy-ICA-Gather-1",
ContractIsLockedDueToLowFactionReputation = false
}, },
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Medium-ICA-Uplink-1", ContractId = "NEW-Medium-ICA-Uplink-1",
ContractIsLockedDueToLowFactionReputation = false
}, },
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Hard-ICA-Uplink-1", ContractId = "NEW-Hard-ICA-Uplink-1",
ContractIsLockedDueToLowFactionReputation = false
} }
} }
}, },
@@ -61,17 +58,14 @@ public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsR
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Easy-KOR-Mine-4", ContractId = "NEW-Easy-KOR-Mine-4",
ContractIsLockedDueToLowFactionReputation = false
}, },
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Medium-KOR-Mine-1", ContractId = "NEW-Medium-KOR-Mine-1",
ContractIsLockedDueToLowFactionReputation = false
}, },
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Hard-KOR-PvP-6", ContractId = "NEW-Hard-KOR-PvP-6",
ContractIsLockedDueToLowFactionReputation = false
} }
} }
}, },
@@ -83,17 +77,14 @@ public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsR
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Easy-Osiris-Brightcaps-1", ContractId = "NEW-Easy-Osiris-Brightcaps-1",
ContractIsLockedDueToLowFactionReputation = false
}, },
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Medium-Osiris-Gather-1", ContractId = "NEW-Medium-Osiris-Gather-1",
ContractIsLockedDueToLowFactionReputation = false
}, },
new FYFactionContractData new FYFactionContractData
{ {
ContractId = "NEW-Hard-Osiris-Gather-7", ContractId = "NEW-Hard-Osiris-Gather-7",
ContractIsLockedDueToLowFactionReputation = false
} }
} }
} }
@@ -0,0 +1,65 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class FYMatchConnectionData {
[JsonPropertyName("addr")]
public string Addr { get; set; }
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("serverId")]
public string ServerID { get; set; }
[JsonPropertyName("connectSinglePlayer")]
public bool ConnectSinglePlayer { get; set; }
[JsonPropertyName("isMatch")]
public bool IsMatch { get; set; }
}
public class RequestServerSessionStateRequest
{
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("amountRequests")]
public int AmountRequests { get; set; }
}
public class RequestServerSessionStateResponse
{
[JsonPropertyName("connectionData")]
public FYMatchConnectionData ConnectionData { get; set; }
[JsonPropertyName("retryCounter")]
public int RetryCounter { get; set; }
[JsonPropertyName("canGoToSession")]
public bool CanGoToSession { get; set; }
[JsonPropertyName("shouldCancel")]
public bool ShouldCancel { get; set; }
}
[CloudScriptFunction("RequestServerSessionState")]
public class RequestServerSessionStateFunction : ICloudScriptFunction<RequestServerSessionStateRequest, RequestServerSessionStateResponse>
{
private readonly ILogger<RequestServerSessionStateFunction> _logger;
public RequestServerSessionStateFunction(ILogger<RequestServerSessionStateFunction> logger)
{
_logger = logger;
}
public async Task<RequestServerSessionStateResponse> ExecuteAsync(RequestServerSessionStateRequest request)
{
_logger.LogInformation("Processing server session state");
return new RequestServerSessionStateResponse
{
CanGoToSession = true,
ConnectionData = new FYMatchConnectionData {
Addr = request.SessionID, // FIXME: This is for offline use case only!,
ConnectSinglePlayer = false,
IsMatch = true,
ServerID = "testserver",
SessionID = request.SessionID,
},
ShouldCancel = false,
RetryCounter = 1,
};
}
}
@@ -0,0 +1,61 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestStoreRotationDataRequest
{
// Empty request
}
public class StoreRotationData
{
[JsonPropertyName("storeId")]
public string StoreID { get; set; }
[JsonPropertyName("expirationData")]
public DateTime ExpirationData { get; set; }
}
public class RequestStoreRotationDataResponse
{
[JsonPropertyName("dailyStoreData")]
public StoreRotationData DailyStoreData { get; set; }
[JsonPropertyName("weeklyStoreData")]
public StoreRotationData WeeklyStoreData { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
}
[CloudScriptFunction("RequestStoreRotationData")]
public class RequestStoreRotationDataFunction : ICloudScriptFunction<RequestStoreRotationDataRequest, RequestStoreRotationDataResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public RequestStoreRotationDataFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestStoreRotationDataResponse> ExecuteAsync(RequestStoreRotationDataRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
// TODO: Get and save store rotation data
return new RequestStoreRotationDataResponse
{
Error = "",
DailyStoreData = new StoreRotationData {
StoreID = "DailyShop",
ExpirationData = DateTime.UtcNow.AddDays(1)
},
WeeklyStoreData = new StoreRotationData {
StoreID = "WeeklyShop",
ExpirationData = DateTime.UtcNow.AddDays(1)
}
};
}
}
@@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestUpdateAvailableFreeLoadoutRequest
{
}
public class RequestUpdateAvailableFreeLoadoutResponse
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("newRandomSeed")]
public int NewRandomSeed { get; set; }
}
[CloudScriptFunction("RequestUpdateAvailableFreeLoadout")]
public class RequestUpdateAvailableFreeLoadoutFunction : ICloudScriptFunction<RequestUpdateAvailableFreeLoadoutRequest, RequestUpdateAvailableFreeLoadoutResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<RequestUpdateAvailableFreeLoadoutFunction> _logger;
public RequestUpdateAvailableFreeLoadoutFunction(ILogger<RequestUpdateAvailableFreeLoadoutFunction> logger, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestUpdateAvailableFreeLoadoutResponse> ExecuteAsync(RequestUpdateAvailableFreeLoadoutRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
return new RequestUpdateAvailableFreeLoadoutResponse
{
UserId = userId,
Error = "",
NewRandomSeed = 0,
};
}
}
@@ -0,0 +1,44 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestUpdateSeasonWipeDataRequest
{
}
public class RequestUpdateSeasonWipeDataResponse
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
}
[CloudScriptFunction("RequestUpdateSeasonWipeData")]
public class RequestUpdateSeasonWipeDataFunction : ICloudScriptFunction<RequestUpdateSeasonWipeDataRequest, RequestUpdateSeasonWipeDataResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<RequestUpdateSeasonWipeDataFunction> _logger;
public RequestUpdateSeasonWipeDataFunction(ILogger<RequestUpdateSeasonWipeDataFunction> logger, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestUpdateSeasonWipeDataResponse> ExecuteAsync(RequestUpdateSeasonWipeDataRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
return new RequestUpdateSeasonWipeDataResponse
{
UserId = userId,
Error = "",
};
}
}
@@ -0,0 +1,123 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
public class LoadoutData {
[JsonPropertyName("shield")]
public string Shield { get; set; }
[JsonPropertyName("helmet")]
public string Helmet { get; set; }
[JsonPropertyName("weaponOne")]
public string WeaponOne { get; set; }
[JsonPropertyName("weaponTwo")]
public string WeaponTwo { get; set; }
[JsonPropertyName("bag")]
public string Bag { get; set; }
[JsonPropertyName("bagItemsAsJsonStr")]
public string BagItemsAsJsonStr { get; set; }
[JsonPropertyName("safeItemsAsJsonStr")]
public string SafeItemsAsJsonStr { get; set; }
}
public class RequestUpdateStationInventoryRequest
{
[JsonPropertyName("newSet")]
public LoadoutData NewSet { get; set; }
[JsonPropertyName("itemsToAdd")]
public FYCustomItemInfo[] ItemsToAdd { get; set; }
[JsonPropertyName("itemsToUpdateAmount")]
public FYCustomItemInfo[] ItemsToUpdateAmount { get; set; }
[JsonPropertyName("itemsToRemove")]
public HashSet<string> ItemsToRemove { get; set; }
}
public class RequestUpdateStationInventoryResponse
{
// [JsonPropertyName("userId")]
// public string UserId { get; set; }
// [JsonPropertyName("error")]
// public string Error { get; set; }
// [JsonPropertyName("newSet")]
// public LoadoutData NewSet { get; set; }
// [JsonPropertyName("itemsToAdd")]
// public FYCustomItemInfo[] ItemsToAdd { get; set; }
// [JsonPropertyName("itemsToUpdateAmount")]
// public string[] ItemsToUpdateAmount { get; set; }
// [JsonPropertyName("itemsToRemove")]
// public string[] ItemsToRemove { get; set; }
}
[CloudScriptFunction("RequestUpdateStationInventory")]
public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<RequestUpdateStationInventoryRequest, RequestUpdateStationInventoryResponse>
{
private readonly ILogger<RequestUpdateStationInventoryFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestUpdateStationInventoryFunction(ILogger<RequestUpdateStationInventoryFunction> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<RequestUpdateStationInventoryResponse> ExecuteAsync(RequestUpdateStationInventoryRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory"});
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
// TODO: Optimize
// TODO: Check deleted items to see if other stacks/mods were updated correctly
// Process inventory update. The player may:
// 1. Split item stack - creates a new item 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.
// 4. Change weapon mods - updates an existing item.
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!request.ItemsToRemove.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
foreach (var item in request.ItemsToUpdateAmount) {
var inventoryItem = newInventory.Find(i => i.ItemId == item.ItemId);
if (inventoryItem == null) {
continue;
}
inventoryItem.Amount = item.Amount;
inventoryItem.ModData = item.ModData;
inventoryItem.PrimaryVanityId = item.PrimaryVanityId;
inventoryItem.SecondaryVanityId = item.SecondaryVanityId;
}
// TODO: Check updated items and validate new items
foreach (var item in request.ItemsToAdd) {
newInventory.Add(item);
}
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>{
["LOADOUT"] = JsonSerializer.Serialize(request.NewSet),
["Inventory"] = JsonSerializer.Serialize(newInventory),
});
return new RequestUpdateStationInventoryResponse
{
// UserId = userId,
// Error = "",
// NewSet = request.NewSet,
// ItemsToAdd = [],
// ItemsToRemove = request.ItemsToRemove,
// ItemsToUpdateAmount = request.ItemsToUpdateAmount,
};
}
}
@@ -0,0 +1,45 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RegionObject
{
[JsonPropertyName("region")]
public string Region { get; set; }
[JsonPropertyName("ping")]
public int Ping { get; set; }
[JsonPropertyName("instanceType")]
public int InstanceType { get; set; }
}
public class RequestUsersPingRequestRequest
{
[JsonPropertyName("pings")]
public RegionObject[] Pings { get; set; }
[JsonPropertyName("countryCode")]
public string CountryCode { get; set; }
}
public class RequestUsersPingRequestResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("RequestUsersPingRequest")]
public class RequestUsersPingRequestFunction : ICloudScriptFunction<RequestUsersPingRequestRequest, RequestUsersPingRequestResponse>
{
private readonly ILogger<RequestUsersPingRequestFunction> _logger;
public RequestUsersPingRequestFunction(ILogger<RequestUsersPingRequestFunction> logger)
{
_logger = logger;
}
public async Task<RequestUsersPingRequestResponse> ExecuteAsync(RequestUsersPingRequestRequest request)
{
_logger.LogInformation("Processing ping request");
return new RequestUsersPingRequestResponse
{
};
}
}
@@ -0,0 +1,71 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class ResetActionsCapPlayerRequest
{
// Empty request
}
public class ResetActionsCapPlayerResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
}
public class FortunaPassDailyCompletedActions {
[JsonPropertyName("actions")]
public object Actions { get; set; }
[JsonPropertyName("lastDailyCapResetTimeUtc")]
public FYTimestamp LastDailyCapResetTimeUtc { get; set; }
}
[CloudScriptFunction("ResetActionsCapPlayer")]
public class ResetActionsCapPlayer : ICloudScriptFunction<ResetActionsCapPlayerRequest, ResetActionsCapPlayerResponse>
{
private readonly UserDataService _userDataService;
private readonly IHttpContextAccessor _httpContextAccessor;
public ResetActionsCapPlayer(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<ResetActionsCapPlayerResponse> ExecuteAsync(ResetActionsCapPlayerRequest 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 userData = await _userDataService.FindAsync(userId, userId, new List<string>{"FortunaPass2_DailyCompletedActions"});
var dailyCompletedActions = JsonSerializer.Deserialize<FortunaPassDailyCompletedActions>(userData["FortunaPass2_DailyCompletedActions"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now <= dailyCompletedActions.LastDailyCapResetTimeUtc.Seconds + 86400) {
return new ResetActionsCapPlayerResponse {
UserID = userId,
Error = "",
};
}
dailyCompletedActions.LastDailyCapResetTimeUtc.Seconds = now;
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>{
["FortunaPass2_DailyCompletedActions"] = JsonSerializer.Serialize(dailyCompletedActions), // Season 2
["FortunaPass3_DailyCompletedActions"] = JsonSerializer.Serialize(dailyCompletedActions), // Season 3
});
return new ResetActionsCapPlayerResponse{
UserID = userId,
Error = "",
};
}
}
@@ -0,0 +1,171 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
public class SellItemsClientRequest
{
[JsonPropertyName("ids")]
public string[] IDs { get; set; }
[JsonPropertyName("factionId")]
public string FactionID { get; set; }
[JsonPropertyName("inventoryUpdateData")]
public SellItemsClientInventoryData InventoryUpdateData { get; set; }
}
public class SellItemsClientInventoryData {
[JsonPropertyName("itemsToAdd")]
public FYCustomItemInfo[] ItemsToAdd { get; set; }
[JsonPropertyName("itemsToUpdateAmount")]
public FYCustomItemInfo[] ItemsToUpdateAmount { get; set; }
[JsonPropertyName("itemsToRemove")]
public HashSet<string> ItemsToRemove { get; set; }
}
public class SellItemsClientResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("scrappedItemIds")]
public HashSet<string> ScrappedItemIDs { get; set; }
[JsonPropertyName("changedItems")]
public List<FYCustomItemInfo> ChangedItems { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("playerFactionProgressionData")]
public FYPlayerFactionProgressData PlayerFactionProgressionData { get; set; }
}
public class FYPlayerFactionProgressData {
[JsonPropertyName("factionId")]
public string FactionID { get; set; }
[JsonPropertyName("currentProgression")]
public int CurrentProgression { get; set; }
}
[CloudScriptFunction("SellItemsClient")]
public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientRequest, SellItemsClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<SellItemsClientFunction> _logger;
public SellItemsClientFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger<SellItemsClientFunction> logger)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<SellItemsClientResponse> ExecuteAsync(SellItemsClientRequest 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 progressionFactionKey = $"FactionProgression{request.FactionID}";
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance", progressionFactionKey});
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var factionProgression = JsonSerializer.Deserialize<int>(userData[progressionFactionKey].Value);
var blueprintsData = _titleDataService.Find(new List<string>{"Blueprints"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(blueprintsData["Blueprints"]);
// TODO: Optimize
// TODO: Check deleted items to see if other stacks/mods were updated correctly
// Process inventory update before selling since a player may split item stack.
// This will result in creating a new item and updating amount of existing item.
// NOTE: Stacking items back doesn't seem to work, at least in single-player station. Bug?
// var newInventory = new List<FYCustomItemInfo>(inventory.Count);
// foreach (var item in inventory) {
// if (!request.InventoryUpdateData.ItemsToRemove.Contains(item.ItemId)) {
// newInventory.Add(item);
// }
// }
var changedItems = new List<FYCustomItemInfo>();
foreach (var item in request.InventoryUpdateData.ItemsToUpdateAmount) {
var inventoryItem = inventory.Find(i => i.ItemId == item.ItemId);
if (inventoryItem == null) {
continue;
}
inventoryItem.Amount = item.Amount;
// NOTE: Vanity and mod data cannot be managed in sell menu
changedItems.Add(item);
}
foreach (var item in request.InventoryUpdateData.ItemsToAdd) {
inventory.Add(item);
}
// Then process selling. The game client provides item IDs for newly added items.
HashSet<string> scrappedItemIds = [];
foreach (var itemId in request.IDs) {
var inventoryItem = inventory.Find(i => i.ItemId == itemId);
if (inventoryItem == null) {
continue;
}
var blueprintData = blueprints[inventoryItem.BaseItemId];
// TODO: Probably better to decide based on item kind instead
if (inventoryItem.Durability == -1) {
factionProgression += (int)((float)blueprintData.OverrideScrappingReputation / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
balance.SoftCurrency += (int)((float)blueprintData.OverrideScrappingReturns / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
} else {
factionProgression += (int)Math.Max(
(float)inventoryItem.Durability / blueprintData.DurabilityMax * blueprintData.OverrideScrappingReputation,
blueprintData.OverrideScrappingReputation * blueprintData.DurabilityBrokenScrappingReturnModifier
);
balance.SoftCurrency += (int)Math.Max(
(float)inventoryItem.Durability / blueprintData.DurabilityMax * blueprintData.OverrideScrappingReturns,
blueprintData.OverrideScrappingReturns * blueprintData.DurabilityBrokenScrappingReturnModifier
);
}
scrappedItemIds.Add(itemId);
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!scrappedItemIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(newInventory),
["Balance"] = JsonSerializer.Serialize(balance),
[progressionFactionKey] = JsonSerializer.Serialize(factionProgression),
}
);
return new SellItemsClientResponse
{
UserID = userId,
Error = "",
ChangedCurrencies = [new FYCurrencyItem {
CurrencyName = "SoftCurrency",
Amount = balance.SoftCurrency,
}],
PlayerFactionProgressionData = new FYPlayerFactionProgressData {
FactionID = request.FactionID,
CurrentProgression = factionProgression,
},
ScrappedItemIDs = scrappedItemIds,
ChangedItems = changedItems,
};
}
}
@@ -0,0 +1,63 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class UserIdentifiers {
[JsonPropertyName("ip")]
public string IP { get; set; }
[JsonPropertyName("platform")]
public string Platform { get; set; }
[JsonPropertyName("sku")]
public string SKU { get; set; }
[JsonPropertyName("oS")]
public string OS { get; set; }
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
[JsonPropertyName("timezone")]
public string Timezone { get; set; }
[JsonPropertyName("language")]
public string Language { get; set; }
}
public class SendGamesightEventRequest
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("revenue_currency")]
public string RevenueCurrency { get; set; }
[JsonPropertyName("revenue_amount")]
public int RevenueAmount { get; set; }
[JsonPropertyName("identifiers")]
public UserIdentifiers Identifiers { get; set; }
}
public class SendGamesightEventResponse
{
// TODO: Unknown structure. Possibly empty response
}
[CloudScriptFunction("SendGamesightEvent")]
public class SendGamesightEventFunction : ICloudScriptFunction<SendGamesightEventRequest, SendGamesightEventResponse>
{
private readonly ILogger<SendGamesightEventFunction> _logger;
public SendGamesightEventFunction(ILogger<SendGamesightEventFunction> logger)
{
_logger = logger;
}
public async Task<SendGamesightEventResponse> ExecuteAsync(SendGamesightEventRequest request)
{
_logger.LogInformation("Processing Gamesight Event");
return new SendGamesightEventResponse{};
}
}
@@ -0,0 +1,52 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class FYSetActiveGlobalVanityRequest
{
[JsonPropertyName("activeVanity")]
public FYActiveGlobalVanity ActiveVanity { get; set; }
}
public class FYSetActiveGlobalVanityResponse
{
[JsonPropertyName("activeVanity")]
public FYActiveGlobalVanity ActiveVanity { get; set; }
}
[CloudScriptFunction("SetActiveGlobalVanity")]
public class SetActiveGlobalVanityFunction : ICloudScriptFunction<FYSetActiveGlobalVanityRequest, FYSetActiveGlobalVanityResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public SetActiveGlobalVanityFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYSetActiveGlobalVanityResponse> ExecuteAsync(FYSetActiveGlobalVanityRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// TODO: Check vanities availability
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["GlobalVanity"] = JsonSerializer.Serialize(request.ActiveVanity) }
);
return new FYSetActiveGlobalVanityResponse
{
ActiveVanity = request.ActiveVanity,
};
}
}
@@ -0,0 +1,74 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYSetActiveCharacterArchetypeRequest
{
[JsonPropertyName("archetypeId")]
public string ArcheTypeId { get; set; } // E03_G02
}
public class FYSetActiveCharacterArchetypeResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("returnVanity")]
public FYCharacterVanity ReturnVanity { get; set; }
}
[CloudScriptFunction("SetCharacterVanityArchetype")]
public class SetCharacterVanityArchetype : ICloudScriptFunction<FYSetActiveCharacterArchetypeRequest, FYSetActiveCharacterArchetypeResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public SetCharacterVanityArchetype(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYSetActiveCharacterArchetypeResponse> ExecuteAsync(FYSetActiveCharacterArchetypeRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new FYSetActiveCharacterArchetypeResponse{};
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"CharacterVanity"});
var returnVanity = JsonSerializer.Deserialize<FYCharacterVanity>(userData["CharacterVanity"].Value);
// TODO: This works only for Season 2. Season 3 has different IDs
var genderSuffix = request.ArcheTypeId.EndsWith("G02") ? "M" : "F";
returnVanity.ArchetypeID = request.ArcheTypeId;
returnVanity.HeadItem.ID = request.ArcheTypeId + "_Head01";
#if SEASON_2_RELEASE || SEASON_2_DEBUG
returnVanity.BootsItem.ID = $"StarterOutfit01_Boots_{genderSuffix}";
returnVanity.ChestItem.ID = $"StarterOutfit01_Chest_{genderSuffix}";
returnVanity.GloveItem.ID = $"StarterOutfit01_Gloves_{genderSuffix}";
#elif SEASON_3_RELEASE || SEASON_3_DEBUG
returnVanity.BootsItem.ID = "StarterOutfit01_Boots";
returnVanity.ChestItem.ID = "StarterOutfit01_Chest";
returnVanity.GloveItem.ID = "StarterOutfit01_Gloves";
#else
#error Unsupported build type
#endif
returnVanity.BaseSuitItem.ID = $"StarterOutfit01{genderSuffix}_BaseSuit";
returnVanity.BodyType = request.ArcheTypeId.EndsWith("G02") ? 1 : 2;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["CharacterVanity"] = JsonSerializer.Serialize(returnVanity) }
);
return new FYSetActiveCharacterArchetypeResponse
{
Success = true,
ReturnVanity = returnVanity,
};
}
}
@@ -0,0 +1,115 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Functions;
using Prospect.Server.Api.Services.UserData;
public class VanityDataiD {
[JsonPropertyName("iD")]
public string ID { get; set; }
[JsonPropertyName("materialIndex")]
public int MaterialIndex { get; set; }
}
public class SetCharacterVanityItems {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("head_item")]
public VanityDataiD HeadItem { get; set; }
[JsonPropertyName("boots_item")]
public VanityDataiD BootsItem { get; set; }
[JsonPropertyName("chest_item")]
public VanityDataiD ChestItem { get; set; }
[JsonPropertyName("glove_item")]
public VanityDataiD GloveItem { get; set; }
[JsonPropertyName("base_suit_item")]
public VanityDataiD BaseSuitItem { get; set; }
[JsonPropertyName("melee_weapon_item")]
public VanityDataiD MeleeWeaponItem { get; set; }
[JsonPropertyName("body_type")]
public int BodyType { get; set; }
[JsonPropertyName("archetype_id")]
public string ArchetypeID { get; set; }
[JsonPropertyName("slot_index")]
public int SlotIndex { get; set; }
}
public class SetCharacterVanityItemsRequest {
[JsonPropertyName("desiredVanity")]
public SetCharacterVanityItems DesiredVanity { get; set; }
}
public class SetCharacterVanityItemsResponse
{
[JsonPropertyName("returnVanity")]
public FYCharacterVanity ReturnVanity { get; set; }
}
[CloudScriptFunction("SetCharacterVanityItems")]
public class SetCharacterVanityItemsFunction : ICloudScriptFunction<SetCharacterVanityItemsRequest, SetCharacterVanityItemsResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public SetCharacterVanityItemsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<SetCharacterVanityItemsResponse> ExecuteAsync(SetCharacterVanityItemsRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// TODO: Check for available vanity items
// TODO: Refactor
var vanityData = request.DesiredVanity;
var mappedVanityItems = new FYCharacterVanity{
UserID = userId,
BaseSuitItem = new FYVanityMaterialItem{
ID = vanityData.BaseSuitItem.ID,
MaterialIndex = vanityData.BaseSuitItem.MaterialIndex,
},
BootsItem = new FYVanityMaterialItem{
ID = vanityData.BootsItem.ID,
MaterialIndex = vanityData.BootsItem.MaterialIndex,
},
ChestItem = new FYVanityMaterialItem{
ID = vanityData.ChestItem.ID,
MaterialIndex = vanityData.ChestItem.MaterialIndex,
},
GloveItem = new FYVanityMaterialItem{
ID = vanityData.GloveItem.ID,
MaterialIndex = vanityData.GloveItem.MaterialIndex,
},
HeadItem = new FYVanityMaterialItem{
ID = vanityData.HeadItem.ID,
MaterialIndex = vanityData.HeadItem.MaterialIndex,
},
MeleeWeaponItem = new FYVanityMaterialItem{
ID = vanityData.MeleeWeaponItem.ID,
MaterialIndex = vanityData.MeleeWeaponItem.MaterialIndex,
},
ArchetypeID = vanityData.ArchetypeID,
BodyType = vanityData.BodyType,
SlotIndex = vanityData.SlotIndex
};
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["CharacterVanity"] = JsonSerializer.Serialize(mappedVanityItems) }
);
return new SetCharacterVanityItemsResponse
{
ReturnVanity = mappedVanityItems,
};
}
}
@@ -0,0 +1,161 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYSkipItemCraftingClientRequest {
[JsonPropertyName("useOptionalCosts")]
public bool UseOptionalCosts { get; set; }
}
public class FYSkipItemCraftingClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("craftedItemData")]
public FYCraftedItemData CraftedItemData { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
}
public class FYCraftedItemData
{
[JsonPropertyName("itemsGrantedOrUpdated")]
public List<FYCustomItemInfo> ItemsGrantedOrUpdated { get; set; }
[JsonPropertyName("BlueprintName")]
public string BlueprintName { get; set; }
};
[CloudScriptFunction("SkipItemCraftingClient")]
public class SkipItemCraftingClient : ICloudScriptFunction<FYSkipItemCraftingClientRequest, FYSkipItemCraftingClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public SkipItemCraftingClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYSkipItemCraftingClientResult> ExecuteAsync(FYSkipItemCraftingClientRequest 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>{"Blueprints"});
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"CraftingTimer__2022_05_12", "Balance", "Inventory"}
);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var craftingTimer = JsonSerializer.Deserialize<FYItemCurrentlyBeingCrafted>(userData["CraftingTimer__2022_05_12"].Value);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var blueprintName = craftingTimer.ItemID;
var blueprintData = blueprints[blueprintName];
// Validate price
var shopCraftingData = blueprintData.ItemShopsCraftingData["CraftingStation"];
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var craftEndTime = craftingTimer.UtcTimestampWhenCraftingStarted.Seconds + shopCraftingData.UpgradeTimeSeconds;
var craftStartTime = craftingTimer.UtcTimestampWhenCraftingStarted.Seconds;
FYCurrencyItem[] changedCurrency;
if (request.UseOptionalCosts) {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, shopCraftingData.SkipOptionalCraftingMaxCost, 1);
if (balance.SoftCurrency < remaining) {
return new FYSkipItemCraftingClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
} else {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, shopCraftingData.SkipCraftingMaxCost, 1);
if (balance.HardCurrency < remaining) {
return new FYSkipItemCraftingClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.HardCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
}
List<FYCustomItemInfo> itemsGrantedOrUpdated = [];
var remainingAmount = blueprintData.AmountPerPurchase;
// Populate incomplete stacks first
foreach (var item in inventory) {
if (item.BaseItemId != blueprintName || item.Amount >= blueprintData.MaxAmountPerStack) {
continue;
}
var remainingSpace = blueprintData.MaxAmountPerStack - item.Amount;
var amountToAdd = Math.Min(remainingAmount, remainingSpace);
item.Amount += amountToAdd;
remainingAmount -= amountToAdd;
itemsGrantedOrUpdated.Add(item);
if (remainingAmount == 0) {
break;
}
}
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
var craftedItem = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = blueprintName,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(craftedItem);
itemsGrantedOrUpdated.Add(craftedItem);
remainingAmount -= itemStackAmount;
}
craftingTimer.ItemID = "";
craftingTimer.UtcTimestampWhenCraftingStarted.Seconds = 0;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(inventory),
["CraftingTimer__2022_05_12"] = JsonSerializer.Serialize(craftingTimer),
}
);
return new FYSkipItemCraftingClientResult
{
UserID = userId,
Error = "",
CraftedItemData = new FYCraftedItemData {
BlueprintName = blueprintName,
ItemsGrantedOrUpdated = itemsGrantedOrUpdated,
},
ChangedCurrencies = changedCurrency,
};
}
}
@@ -0,0 +1,129 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYSkipPlayerQuarterUpgradeClientRequest {
[JsonPropertyName("useOptionalCosts")]
public bool UseOptionalCosts { get; set; }
}
public class FYSkipPlayerQuarterUpgradeClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("newLevel")]
public int NewLevel { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public string[] DeletedItemsIds { get; set; }
}
[CloudScriptFunction("SkipPlayerQuarterUpgradeClient")]
public class SkipPlayerQuarterUpgradeClient : ICloudScriptFunction<FYSkipPlayerQuarterUpgradeClientRequest, FYSkipPlayerQuarterUpgradeClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public SkipPlayerQuarterUpgradeClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYSkipPlayerQuarterUpgradeClientResult> ExecuteAsync(FYSkipPlayerQuarterUpgradeClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"PlayerQuartersLevel", "Balance"}
);
var userPlayerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
if (userPlayerQuarters.UpgradeStartedTime.Seconds == 0) {
return new FYSkipPlayerQuarterUpgradeClientResult
{
UserID = userId,
Error = "Nothing is being constructed",
};
}
var titleData = _titleDataService.Find(new List<string>{"PlayerQuarters"});
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var playerQuartersData = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
var nextQuartersLevel = playerQuartersData[userPlayerQuarters.Level - 1]; // Quarters level starts from 1
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var craftEndTime = userPlayerQuarters.UpgradeStartedTime.Seconds + nextQuartersLevel.UpgradeSeconds;
var craftStartTime = userPlayerQuarters.UpgradeStartedTime.Seconds;
FYCurrencyItem[] changedCurrency;
if (request.UseOptionalCosts) {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nextQuartersLevel.OptionalRushCosts, 1);
if (balance.SoftCurrency < remaining) {
return new FYSkipPlayerQuarterUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
} else {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nextQuartersLevel.InitialRushCosts, 1);
if (balance.HardCurrency < remaining) {
return new FYSkipPlayerQuarterUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.HardCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
}
var upgradeStartedTime = new FYTimestamp {
Seconds = 0,
};
userPlayerQuarters.Level++;
userPlayerQuarters.UpgradeStartedTime = upgradeStartedTime;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["PlayerQuartersLevel"] = JsonSerializer.Serialize(userPlayerQuarters),
}
);
return new FYSkipPlayerQuarterUpgradeClientResult
{
UserID = userId,
Error = "",
NewLevel = userPlayerQuarters.Level,
UpgradeStartedTime = upgradeStartedTime,
RemainingTimeInSeconds = 0,
ChangedItems = [],
DeletedItemsIds = [],
ChangedCurrencies = changedCurrency,
};
}
}
@@ -0,0 +1,160 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYSkipTechTreeNodeUpgradeClientRequest {
[JsonPropertyName("useOptionalCosts")]
public bool UseOptionalCosts { get; set; }
}
public class FYSkipTechTreeNodeUpgradeClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("upgradedNode")]
public FYTechTreeNodeStatus UpgradedNode { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public string[] DeletedItemsIds { get; set; }
}
[CloudScriptFunction("SkipTechTreeNodeUpgradeClient")]
public class SkipTechTreeNodeUpgradeClient : ICloudScriptFunction<FYSkipTechTreeNodeUpgradeClientRequest, FYSkipTechTreeNodeUpgradeClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public SkipTechTreeNodeUpgradeClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYSkipTechTreeNodeUpgradeClientResult> ExecuteAsync(FYSkipTechTreeNodeUpgradeClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"TechTreeNodeData", "Balance", "CharacterTechTreeBonuses"}
);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
var currentNode = userTechTree.NodeInProgress;
if (currentNode == "") {
return new FYSkipTechTreeNodeUpgradeClientResult
{
UserID = userId,
Error = "Nothing is being constructed",
};
}
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes"});
var techTreeNodes = JsonSerializer.Deserialize<Dictionary<string, TitleDataTechTreeNodeInfo>>(titleData["TechTreeNodes"]);
var node = userTechTree.Nodes[currentNode];
var nodeInfo = techTreeNodes[currentNode];
var nodeLevelInfo = techTreeNodes[currentNode].PerkLevels[node.Level]; // Current node level - next upgradable level due to index offset
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var craftEndTime = node.UpgradeStartedTime.Seconds + nodeLevelInfo.UpgradeSeconds;
var craftStartTime = node.UpgradeStartedTime.Seconds;
FYCurrencyItem[] changedCurrency;
if (request.UseOptionalCosts) {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nodeLevelInfo.OptionalRushCosts, 1);
if (balance.SoftCurrency < remaining) {
return new FYSkipTechTreeNodeUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
} else {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nodeLevelInfo.InitialRushCosts, 1);
if (balance.HardCurrency < remaining) {
return new FYSkipTechTreeNodeUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.HardCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
}
userTechTree.TotalUpgrades++;
userTechTree.NodeInProgress = "";
node.Level++;
node.UpgradeStartedTime.Seconds = 0;
var techTreeBonuses = JsonSerializer.Deserialize<CharacterTechTreeBonuses>(userData["CharacterTechTreeBonuses"].Value);
switch (nodeInfo.NodePerkType) {
case EYTechTreeNodePerkType.DailyCrate:
techTreeBonuses.CrateTier += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.IncreaseStashSize:
techTreeBonuses.AddStashSize += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.IncreaseBagSize:
techTreeBonuses.AddSafePocketSize += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.PassiveKMarkGenHour:
techTreeBonuses.KmarksRate += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.IncreasePassiveKMarkGenCap:
techTreeBonuses.KmarksCap += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.PassiveAurumGenDay:
techTreeBonuses.AurumRate = (float)Math.Round(nodeLevelInfo.PerkAmount + techTreeBonuses.AurumRate, 2);
break;
case EYTechTreeNodePerkType.IncreasePassiveAurumGenCap:
techTreeBonuses.AurumCap += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.ReduceUpgradingTimePerc:
techTreeBonuses.UpgradeSpeedFactor = (float)Math.Round(nodeLevelInfo.PerkAmount + techTreeBonuses.UpgradeSpeedFactor, 2);
break;
default:
break;
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["TechTreeNodeData"] = JsonSerializer.Serialize(userTechTree),
["CharacterTechTreeBonuses"] = JsonSerializer.Serialize(techTreeBonuses),
}
);
return new FYSkipTechTreeNodeUpgradeClientResult
{
UserID = userId,
Error = "",
UpgradedNode = node,
RemainingTimeInSeconds = 0,
ChangedItems = [],
DeletedItemsIds = [],
ChangedCurrencies = changedCurrency,
};
}
}
@@ -0,0 +1,70 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.Auth.Extensions;
using Microsoft.AspNetCore.SignalR;
using Prospect.Server.Api.Hubs;
public class SquadMemberReadyForMatchRequest
{
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
[JsonPropertyName("matchmakingSettings")]
public FYUserMatchmakingSettings matchmakingSettings { get; set; }
}
public class SquadMemberReadyForMatchResponse
{
[JsonPropertyName("result")]
public int Result { get; set; } // EYSquadActionResult
[JsonPropertyName("squad")]
public FYPlayFabSquad Squad { get; set; }
[JsonPropertyName("isSquadReadyForMatch")]
public bool IsSquadReadyForMatch { get; set; }
}
[CloudScriptFunction("SquadMemberReadyForMatch")]
public class SquadMemberReadyForMatchFunction : ICloudScriptFunction<SquadMemberReadyForMatchRequest, SquadMemberReadyForMatchResponse>
{
private readonly ILogger<SquadMemberReadyForMatchFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHubContext<CycleHub> _hubContext;
public SquadMemberReadyForMatchFunction(ILogger<SquadMemberReadyForMatchFunction> logger, IHttpContextAccessor httpContextAccessor, IHubContext<CycleHub> hubContext)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_hubContext = hubContext;
}
public async Task<SquadMemberReadyForMatchResponse> ExecuteAsync(SquadMemberReadyForMatchRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
return new SquadMemberReadyForMatchResponse
{
Result = 0, // EYSquadActionResult::OK
Squad = new FYPlayFabSquad {
SquadID = "100", // TODO
// Members = [
// new FYPlayFabSquadMember {
// Profile = new FYPlayFabPlayerProfile {
// PlayerId = userId,
// },
// onlineState = 0, // EYUserState::IN_STATION
// matchmakingSettings = new FYUserMatchmakingSettings {
// isReadyForMatch = true,
// isSecretLeader = true,
// selectedMapName = "Map01",
// }
// }
// ],
},
IsSquadReadyForMatch = true,
};
}
}
@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class SquadMemberSelectedMapRequest
{
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
[JsonPropertyName("selectedMapName")]
public string SelectedMapName { get; set; }
}
public class SquadMemberSelectedMapResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("SquadMemberSelectedMap")]
public class SquadMemberSelectedMapFunction : ICloudScriptFunction<SquadMemberSelectedMapRequest, SquadMemberSelectedMapResponse>
{
private readonly ILogger<SquadMemberSelectedMapFunction> _logger;
public SquadMemberSelectedMapFunction(ILogger<SquadMemberSelectedMapFunction> logger)
{
_logger = logger;
}
public async Task<SquadMemberSelectedMapResponse> ExecuteAsync(SquadMemberSelectedMapRequest request)
{
return new SquadMemberSelectedMapResponse
{};
}
}
@@ -0,0 +1,29 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class SquadMemberStartingDeployFlowRequest
{
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
}
public class SquadMemberStartingDeployFlowResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("SquadMemberStartingDeployFlow")]
public class SquadMemberStartingDeployFlowFunction : ICloudScriptFunction<SquadMemberStartingDeployFlowRequest, SquadMemberStartingDeployFlowResponse>
{
private readonly ILogger<SquadMemberStartingDeployFlowFunction> _logger;
public SquadMemberStartingDeployFlowFunction(ILogger<SquadMemberStartingDeployFlowFunction> logger)
{
_logger = logger;
}
public async Task<SquadMemberStartingDeployFlowResponse> ExecuteAsync(SquadMemberStartingDeployFlowRequest request)
{
return new SquadMemberStartingDeployFlowResponse
{};
}
}
@@ -0,0 +1,156 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYStartItemCraftingClientRequest {
[JsonPropertyName("baseItemId")]
public string BaseItemID { get; set; }
}
public class FYStartItemCraftingClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public List<FYCustomItemInfo> ChangedItems { get; set; }
[JsonPropertyName("deletedItemsIds")]
public HashSet<string> DeletedItemsIds { get; set; }
}
public class FYItemCurrentlyBeingCrafted {
[JsonPropertyName("itemId")]
public string ItemID { get; set; }
[JsonPropertyName("utcTimestampWhenCraftingStarted")]
public FYTimestamp UtcTimestampWhenCraftingStarted { get; set; }
}
[CloudScriptFunction("StartItemCraftingClient")]
public class StartItemCraftingClient : ICloudScriptFunction<FYStartItemCraftingClientRequest, FYStartItemCraftingClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public StartItemCraftingClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYStartItemCraftingClientResult> ExecuteAsync(FYStartItemCraftingClientRequest 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>{"Blueprints"});
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"CraftingTimer__2022_05_12", "Balance", "Inventory"}
);
var craftingTimer = JsonSerializer.Deserialize<FYItemCurrentlyBeingCrafted>(userData["CraftingTimer__2022_05_12"].Value);
if (craftingTimer.UtcTimestampWhenCraftingStarted.Seconds != 0) {
return new FYStartItemCraftingClientResult
{
UserID = userId,
Error = "Crafting is in progress",
};
}
// TODO: Contract unlock criteria
// TODO: Inventory limit check
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var blueprintData = blueprints[request.BaseItemID];
// Validate price
var shopCraftingData = blueprintData.ItemShopsCraftingData["CraftingStation"];
// TODO: Refactor
List<FYCustomItemInfo> changedItems = [];
HashSet<string> deletedItemsIds = [];
List<FYCurrencyItem> changedCurrencies = [];
foreach (var ingredient in shopCraftingData.ItemRecipeIngredients) {
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
remaining -= ingredient.Amount;
balance.SoftCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (ingredient.Currency == "Aurum") {
remaining -= ingredient.Amount;
balance.HardCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else {
foreach (var item in inventory) {
if (item.BaseItemId != ingredient.Currency) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
} else {
changedItems.Add(item);
}
if (remaining <= 0) {
break;
}
}
}
if (remaining > 0) {
return new FYStartItemCraftingClientResult
{
UserID = userId,
Error = "Missing required items",
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
craftingTimer.ItemID = request.BaseItemID;
craftingTimer.UtcTimestampWhenCraftingStarted.Seconds = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(newInventory),
["CraftingTimer__2022_05_12"] = JsonSerializer.Serialize(craftingTimer),
}
);
return new FYStartItemCraftingClientResult
{
UserID = userId,
Error = "",
ChangedItems = changedItems,
DeletedItemsIds = deletedItemsIds,
ChangedCurrencies = changedCurrencies.ToArray(),
};
}
}
@@ -0,0 +1,155 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYStartPlayerQuarterUpgradeClientRequest {
}
public class FYStartPlayerQuarterUpgradeClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("newLevel")]
public int NewLevel { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public List<FYCustomItemInfo> ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public HashSet<string> DeletedItemsIds { get; set; }
}
[CloudScriptFunction("StartPlayerQuarterUpgradeClient")]
public class StartPlayerQuarterUpgradeClient : ICloudScriptFunction<FYStartPlayerQuarterUpgradeClientRequest, FYStartPlayerQuarterUpgradeClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public StartPlayerQuarterUpgradeClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYStartPlayerQuarterUpgradeClientResult> ExecuteAsync(FYStartPlayerQuarterUpgradeClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"PlayerQuartersLevel", "Balance", "Inventory"}
);
var userPlayerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
if (userPlayerQuarters.UpgradeStartedTime.Seconds != 0) {
return new FYStartPlayerQuarterUpgradeClientResult
{
UserID = userId,
Error = "Player quarters upgrade is in progress",
};
}
var titleData = _titleDataService.Find(new List<string>{"PlayerQuarters"});
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var playerQuartersData = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
// Current level contains the information about next level
// Player quarter level starts from 1
var nextQuartersLevel = playerQuartersData[userPlayerQuarters.Level - 1];
// Validate price
// TODO: Refactor
List<FYCustomItemInfo> changedItems = [];
HashSet<string> deletedItemsIds = [];
List<FYCurrencyItem> changedCurrencies = [];
foreach (var ingredient in nextQuartersLevel.UpgradeCosts) {
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
remaining -= ingredient.Amount;
balance.SoftCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (ingredient.Currency == "Aurum") {
remaining -= ingredient.Amount;
balance.HardCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else {
foreach (var item in inventory) {
if (item.BaseItemId != ingredient.Currency) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
} else {
changedItems.Add(item);
}
if (remaining <= 0) {
break;
}
}
}
if (remaining > 0) {
return new FYStartPlayerQuarterUpgradeClientResult
{
UserID = userId,
Error = "Missing required items",
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
var startTime = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var upgradeStartedTime = new FYTimestamp {
Seconds = startTime
};
userPlayerQuarters.UpgradeStartedTime = upgradeStartedTime;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(inventory),
["PlayerQuartersLevel"] = JsonSerializer.Serialize(userPlayerQuarters),
}
);
return new FYStartPlayerQuarterUpgradeClientResult
{
UserID = userId,
Error = "",
NewLevel = userPlayerQuarters.Level++,
UpgradeStartedTime = upgradeStartedTime,
RemainingTimeInSeconds = nextQuartersLevel.UpgradeSeconds,
ChangedItems = changedItems,
DeletedItemsIds = deletedItemsIds,
ChangedCurrencies = changedCurrencies.ToArray(),
};
}
}
@@ -0,0 +1,167 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYStartTechTreeNodeUpgradeClientRequest {
[JsonPropertyName("nodeId")]
public string NodeID { get; set; }
}
public class FYStartTechTreeNodeUpgradeClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("upgradedNode")]
public FYTechTreeNodeStatus UpgradedNode { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public List<FYCustomItemInfo> ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public HashSet<string> DeletedItemsIds { get; set; }
}
[CloudScriptFunction("StartTechTreeNodeUpgradeClient")]
public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTreeNodeUpgradeClientRequest, FYStartTechTreeNodeUpgradeClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public StartTechTreeNodeUpgradeClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYStartTechTreeNodeUpgradeClientResult> ExecuteAsync(FYStartTechTreeNodeUpgradeClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"TechTreeNodeData", "Balance", "Inventory"}
);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
var currentNode = userTechTree.NodeInProgress;
if (currentNode != "") {
return new FYStartTechTreeNodeUpgradeClientResult
{
UserID = userId,
Error = $"{currentNode} is in progress",
};
}
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes"});
var techTreeNodes = JsonSerializer.Deserialize<Dictionary<string, TitleDataTechTreeNodeInfo>>(titleData["TechTreeNodes"]);
// Current level contains the information about next level
var currentLevel = userTechTree.Nodes[request.NodeID].Level;
var techTreeNodeData = techTreeNodes[request.NodeID];
var nextTreeNodeLevel = techTreeNodeData.PerkLevels[currentLevel]; // Tech tree node levels start from 0
// TODO: Dependency check
// Validate price
// TODO: Refactor
List<FYCustomItemInfo> changedItems = [];
HashSet<string> deletedItemsIds = [];
List<FYCurrencyItem> changedCurrencies = [];
foreach (var ingredient in nextTreeNodeLevel.UpgradeCosts) {
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
remaining -= ingredient.Amount;
balance.SoftCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (ingredient.Currency == "Aurum") {
remaining -= ingredient.Amount;
balance.HardCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else {
foreach (var item in inventory) {
if (item.BaseItemId != ingredient.Currency) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
} else {
changedItems.Add(item);
}
if (remaining <= 0) {
break;
}
}
}
if (remaining > 0) {
return new FYStartTechTreeNodeUpgradeClientResult
{
UserID = userId,
Error = "Missing required items",
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
var startTime = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
userTechTree.NodeInProgress = request.NodeID;
var upgradeStartedTime = new FYTimestamp {
Seconds = startTime
};
var upgradedNode = new FYTechTreeNodeStatus {
Level = currentLevel,
NodeID = request.NodeID,
UpgradeStartedTime = upgradeStartedTime,
};
userTechTree.Nodes[request.NodeID] = upgradedNode;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(newInventory),
["TechTreeNodeData"] = JsonSerializer.Serialize(userTechTree),
}
);
return new FYStartTechTreeNodeUpgradeClientResult
{
UserID = userId,
Error = "",
UpgradedNode = upgradedNode,
RemainingTimeInSeconds = nextTreeNodeLevel.UpgradeSeconds,
ChangedItems = changedItems,
DeletedItemsIds = deletedItemsIds,
ChangedCurrencies = changedCurrencies.ToArray(),
};
}
}
@@ -0,0 +1,129 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYStopItemCraftingClientRequest {
[JsonPropertyName("useOptionalCosts")]
public bool UseOptionalCosts { get; set; }
}
public class FYStopItemCraftingClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("craftedItemData")]
public FYCraftedItemData CraftedItemData { get; set; }
}
[CloudScriptFunction("StopItemCraftingClient")]
public class StopItemCraftingClient : ICloudScriptFunction<FYStopItemCraftingClientRequest, FYStopItemCraftingClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public StopItemCraftingClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYStopItemCraftingClientResult> ExecuteAsync(FYStopItemCraftingClientRequest 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>{"Blueprints"});
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"CraftingTimer__2022_05_12", "Balance", "Inventory"}
);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var craftingTimer = JsonSerializer.Deserialize<FYItemCurrentlyBeingCrafted>(userData["CraftingTimer__2022_05_12"].Value);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var blueprintName = craftingTimer.ItemID;
var blueprintData = blueprints[blueprintName];
var shopCraftingData = blueprintData.ItemShopsCraftingData["CraftingStation"];
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var craftEndTime = craftingTimer.UtcTimestampWhenCraftingStarted.Seconds + shopCraftingData.UpgradeTimeSeconds;
if (now < craftEndTime) {
return new FYStopItemCraftingClientResult
{
UserID = userId,
Error = "Required amount of time hasn't passed yet",
};
}
List<FYCustomItemInfo> itemsGrantedOrUpdated = [];
var remainingAmount = blueprintData.AmountPerPurchase;
// Populate incomplete stacks first
foreach (var item in inventory) {
if (item.BaseItemId != blueprintName || item.Amount >= blueprintData.MaxAmountPerStack) {
continue;
}
var remainingSpace = blueprintData.MaxAmountPerStack - item.Amount;
var amountToAdd = Math.Min(remainingAmount, remainingSpace);
item.Amount += amountToAdd;
remainingAmount -= amountToAdd;
itemsGrantedOrUpdated.Add(item);
if (remainingAmount == 0) {
break;
}
}
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
var craftedItem = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = blueprintName,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(craftedItem);
itemsGrantedOrUpdated.Add(craftedItem);
remainingAmount -= itemStackAmount;
}
craftingTimer.ItemID = "";
craftingTimer.UtcTimestampWhenCraftingStarted.Seconds = 0;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(inventory),
["CraftingTimer__2022_05_12"] = JsonSerializer.Serialize(craftingTimer),
}
);
return new FYStopItemCraftingClientResult
{
UserID = userId,
Error = "",
CraftedItemData = new FYCraftedItemData {
BlueprintName = blueprintName,
ItemsGrantedOrUpdated = itemsGrantedOrUpdated,
}
};
}
}
@@ -0,0 +1,153 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYTechTreeNodeUpgradePurchaseClientRequest {
// Empty request
}
public class FYTechTreeNodeUpgradePurchaseClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("upgradedNode")]
public FYTechTreeNodeStatus UpgradedNode { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public string[] DeletedItemsIds { get; set; }
}
public enum EYTechTreeNodePerkType {
None,
IncreaseStashSize,
IncreaseBagSize,
PassiveKMarkGenHour,
PassiveAurumGenDay,
IncreasePassiveKMarkGenCap,
IncreasePassiveAurumGenCap,
DailyCrate,
ReduceUpgradingTimePerc,
MAX
}
[CloudScriptFunction("TechTreeNodeUpgradePurchaseClient")]
public class TechTreeNodeUpgradePurchaseClient : ICloudScriptFunction<FYTechTreeNodeUpgradePurchaseClientRequest, FYTechTreeNodeUpgradePurchaseClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public TechTreeNodeUpgradePurchaseClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYTechTreeNodeUpgradePurchaseClientResult> ExecuteAsync(FYTechTreeNodeUpgradePurchaseClientRequest 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 userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"TechTreeNodeData", "CharacterTechTreeBonuses"}
);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
var currentNode = userTechTree.NodeInProgress;
if (currentNode == "") {
return new FYTechTreeNodeUpgradePurchaseClientResult
{
UserID = userId,
Error = "Nothing is being constructed",
};
}
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes"});
var techTreeNodes = JsonSerializer.Deserialize<Dictionary<string, TitleDataTechTreeNodeInfo>>(titleData["TechTreeNodes"]);
var node = userTechTree.Nodes[currentNode];
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var nodeInfo = techTreeNodes[currentNode];
var nodeLevelInfo = techTreeNodes[currentNode].PerkLevels[node.Level];
var endTime = nodeLevelInfo.UpgradeSeconds + node.UpgradeStartedTime.Seconds;
if (now < endTime) {
return new FYTechTreeNodeUpgradePurchaseClientResult
{
UserID = userId,
Error = "Upgrade is in progress",
};
}
// TODO: Refactor
userTechTree.TotalUpgrades++;
userTechTree.NodeInProgress = "";
node.Level++;
node.UpgradeStartedTime.Seconds = 0;
var techTreeBonuses = JsonSerializer.Deserialize<CharacterTechTreeBonuses>(userData["CharacterTechTreeBonuses"].Value);
switch (nodeInfo.NodePerkType) {
case EYTechTreeNodePerkType.DailyCrate:
techTreeBonuses.CrateTier += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.IncreaseStashSize:
techTreeBonuses.AddStashSize += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.IncreaseBagSize:
techTreeBonuses.AddSafePocketSize += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.PassiveKMarkGenHour:
techTreeBonuses.KmarksRate += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.IncreasePassiveKMarkGenCap:
techTreeBonuses.KmarksCap += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.PassiveAurumGenDay:
techTreeBonuses.AurumRate = (float)Math.Round(nodeLevelInfo.PerkAmount + techTreeBonuses.AurumRate, 2);
break;
case EYTechTreeNodePerkType.IncreasePassiveAurumGenCap:
techTreeBonuses.AurumCap += (int)nodeLevelInfo.PerkAmount;
break;
case EYTechTreeNodePerkType.ReduceUpgradingTimePerc:
techTreeBonuses.UpgradeSpeedFactor = (float)Math.Round(nodeLevelInfo.PerkAmount + techTreeBonuses.UpgradeSpeedFactor, 2);
break;
default:
break;
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["TechTreeNodeData"] = JsonSerializer.Serialize(userTechTree),
["CharacterTechTreeBonuses"] = JsonSerializer.Serialize(techTreeBonuses),
}
);
return new FYTechTreeNodeUpgradePurchaseClientResult
{
UserID = userId,
Error = "",
UpgradedNode = node,
RemainingTimeInSeconds = 0,
ChangedItems = [],
DeletedItemsIds = [],
ChangedCurrencies = [],
};
}
}
@@ -0,0 +1,76 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class TryGetCompleteSquadInfoRequest
{
// Empty request
}
public class FYPlayFabSquad
{
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
[JsonPropertyName("members")]
public FYPlayFabSquadMember[] Members { get; set; }
}
public class FYPlayFabSquadMember {
[JsonPropertyName("profile")]
public FYPlayFabPlayerProfile Profile { get; set; }
[JsonPropertyName("onlineState")]
public int onlineState { get; set; }
[JsonPropertyName("matchmakingSettings")]
public FYUserMatchmakingSettings matchmakingSettings { get; set; }
[JsonPropertyName("mapRowNamesUnlocked")]
public string[] mapRowNamesUnlocked { get; set; }
};
public class FYPlayFabPlayerProfile {
[JsonPropertyName("avatarUrl")]
public string AvatarUrl { get; set; }
[JsonPropertyName("displayName")]
public string DisplayName { get; set; }
[JsonPropertyName("playerId")]
public string PlayerId { get; set; }
};
public class FYUserMatchmakingSettings {
[JsonPropertyName("isReadyForMatch")]
public bool isReadyForMatch;
[JsonPropertyName("selectedMapName")]
public string selectedMapName;
[JsonPropertyName("isSecretLeader")]
public bool isSecretLeader;
[JsonPropertyName("purchaseInsuranceRequest")]
public object purchaseInsuranceRequest; // FYPurchaseInsuranceRequest
};
[CloudScriptFunction("TryGetCompleteSquadInfo")]
public class TryGetCompleteSquadInfoFunction : ICloudScriptFunction<TryGetCompleteSquadInfoRequest, FYPlayFabSquad>
{
private readonly ILogger<TryGetCompleteSquadInfoFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
public TryGetCompleteSquadInfoFunction(ILogger<TryGetCompleteSquadInfoFunction> logger, IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public async Task<FYPlayFabSquad> ExecuteAsync(TryGetCompleteSquadInfoRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
// var userId = context.User.FindAuthUserId();
return new FYPlayFabSquad
{
// SquadID = "100",
// Members = [],
};
}
}

Some files were not shown because too many files have changed in this diff Show More