1 Commits
Author SHA1 Message Date
Yury 44a7554616 Push changes 2025-02-24 01:01:48 +02:00
98 changed files with 595 additions and 4850 deletions
-33
View File
@@ -1,33 +0,0 @@
name: Build & Deploy
on:
push:
branches: [main, preprod]
workflow_dispatch: {}
env:
IMAGE: git.nfteam.ovh/neckfire/the-cycle
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build & push image
run: |
set -e
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login git.nfteam.ovh -u "${{ secrets.REGISTRY_USER }}" --password-stdin
SHA="${GITHUB_SHA::12}"
BRANCH="${GITHUB_REF_NAME}"
if [ "$BRANCH" = "main" ]; then TAGS="-t ${IMAGE}:latest -t ${IMAGE}:${SHA}"; else TAGS="-t ${IMAGE}:${BRANCH} -t ${IMAGE}:${BRANCH}-${SHA}"; fi
docker build $TAGS -f Dockerfile .
docker push --all-tags "${IMAGE}"
- name: Notify ntfy
if: always()
run: |
if [ "${{ job.status }}" = "success" ]; then EMOJI="white_check_mark"; PRIO="default"; else EMOJI="rotating_light"; PRIO="high"; fi
curl -s -H "Authorization: Bearer ${{ secrets.NTFY_TOKEN }}" -H "Title: ${GITHUB_REPOSITORY} — ${{ job.status }}" \
-H "Priority: ${PRIO}" -H "Tags: ${EMOJI}" -H "Click: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions" \
-d "${GITHUB_WORKFLOW} (${GITHUB_REF_NAME} #${GITHUB_RUN_NUMBER}) : ${{ job.status }}" \
"${{ secrets.NTFY_URL }}/${{ secrets.NTFY_TOPIC }}" || true
-1
View File
@@ -1 +0,0 @@
_out/
-15
View File
@@ -1,15 +0,0 @@
# The Cycle: Frontier — serveur API (émulateur Prospect), build depuis les sources.
# Multi-stage : SDK .NET 8 pour publier le projet API → runtime aspnet 8.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY src/ ./src/
# Configuration "Season 2 Release" = build qui correspond au client S2 déployé
# (le code sélectionne la saison via des #if SEASON_2_RELEASE / SEASON_3_RELEASE).
RUN dotnet publish src/Prospect.Server.Api/Prospect.Server.Api.csproj \
-c "Season 2 Release" -o /app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app ./
EXPOSE 8443 8000
ENTRYPOINT ["dotnet", "Prospect.Server.Api.dll"]
-80
View File
@@ -1,80 +0,0 @@
# 🎮 The Cycle: Frontier — Rejoindre le serveur privé (installation client)
Guide pour **installer le client et jouer sur le serveur privé**. Tu n'installes **rien côté serveur** — juste le jeu + un loader + un certificat.
> ⚠️ **À savoir avant de commencer**
> - Le jeu a été retiré de Steam en septembre 2022 : il faut **l'avoir déjà eu dans ta bibliothèque Steam** pour le retélécharger.
> - **Ce n'est pas du co-op en raid.** Chacun joue sa propre instance (stations solo) ; l'émulateur ne supporte pas plusieurs joueurs dans le même raid. Vous partagez le même serveur (progression, boutiques, comptes), pas la même partie.
> - Le serveur doit être **allumé** au moment où tu joues (demande à l'hébergeur).
---
## Fichiers à télécharger (dans la release)
Les 2 fichiers sont dispo dans la **release Gitea** :
👉 **https://git.nfteam.ovh/neckfire/the-cycle/releases/tag/friends-client**
1. **`LoaderPack.zip`** — le loader qui redirige le jeu vers le serveur privé (build b8 / Saison 2).
2. **`certificate.crt`** — le certificat **public** du serveur (sans clé privée, safe).
Télécharge les deux depuis la page de release avant de commencer.
---
## Étapes (Windows)
### 1. Installer le client Saison 2 (via la console Steam)
1. Steam ouvert, `Win+R` → tape `steam://nav/console` → Entrée (la **console Steam** s'ouvre).
2. Colle **exactement** cette commande → Entrée :
```
download_depot 868270 868271 4623363103423775682
```
(`868270` = le jeu, `868271` = le dépôt client, `4623363103423775682` = manifest **Saison 2 v2.7.2**.)
3. Le téléchargement démarre (⚠️ **pas de barre de progression**, sois patient — plusieurs Go).
4. À la fin, Steam affiche **« Depot download complete »** + le **chemin du dossier**
(ex. `...\Steam\steamapps\content\app_868270\depot_868271`). **Ce dossier = ton install du jeu.**
> Il faut **avoir eu The Cycle: Frontier dans ta bibliothèque Steam** avant, sinon → erreur `missing license`.
> **Pas assez d'espace sur le disque principal ?** `download_depot` écrit toujours dans `...\Steam\steamapps\content`. Pour le mettre sur un autre disque : ferme Steam, supprime `...\Steam\steamapps\content`, puis en **cmd admin** :
> ```
> mkdir "D:\SteamContent"
> mklink /J "C:\Program Files (x86)\Steam\steamapps\content" "D:\SteamContent"
> ```
> Rouvre Steam et relance la commande → le jeu ira sur `D:\`.
### 2. Installer le LoaderPack
1. Va dans le dossier du jeu → **`Prospect\Binaries\Win64`**.
2. Ouvre `LoaderPack.zip` et **glisse tout son contenu** dans ce dossier `Win64`.
3. Crée un **raccourci** vers `Prospect.Client.Loader.exe` (c'est avec ça que tu lanceras le jeu).
### 3. Faire confiance au certificat du serveur
1. Double-clique **`certificate.crt`** → **Installer le certificat**.
2. **Emplacement : Utilisateur actuel** → Suivant.
3. **Placer tous les certificats dans le magasin suivant****Parcourir****Autorités de certification racines de confiance** → OK → Suivant → Terminer.
4. Accepte l'avertissement de sécurité (il mentionne `2EA46.playfabapi.com`).
### 4. Pointer le client vers le serveur
Dans **`Prospect\Binaries\Win64`**, crée (ou édite) un fichier **`backend.txt`** contenant **exactement** cette ligne :
```
https://tc.nfteam.ovh:8443
```
### 5. Lancer le jeu
1. **Steam doit être ouvert** (connecté à ton compte).
2. Lance le jeu via le **raccourci `Prospect.Client.Loader`** (⚠️ pas via Steam directement).
3. Connexion automatique avec ton compte Steam. Bienvenue sur Fortuna III ! 🚀
---
## Dépannage
| Symptôme | Cause / solution |
|---|---|
| `libcurl error 7 (Couldn't connect to server)` | Le serveur est **éteint** → demande à l'hébergeur de l'allumer. |
| Erreur de certificat / `hostname mismatch` | `certificate.crt` **pas importé** (ou pas dans « Autorités racines de confiance »). Refais l'étape 3. |
| `missing license` au téléchargement | Tu n'as jamais eu le jeu sur ce compte Steam → impossible de le retélécharger. |
| Parties du corps manquantes (client S3) | Va en station → change ton apparence une fois (corrige les IDs). |
---
*Serveur privé The Cycle: Frontier — émulateur Prospect. Usage privé entre amis.*
+66 -33
View File
@@ -9,10 +9,12 @@ Also known as "The Cycle: Frontier".
- [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. Generate and import SSL certificate](#3-generate-and-import-ssl-certificate)
- [4. Extract `LoaderPack` to the game](#4-extract-loaderpack-to-the-game)
- [5. Run the server](#5-run-the-server)
- [6. Run the game](#6-run-the-game)
- [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. Create a game shortcut with specified arguments](#6-create-a-game-shortcut-with-specified-arguments)
- [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)
@@ -28,7 +30,10 @@ Also known as "The Cycle: Frontier".
* [x] EULA acceptance
* [x] Tutorial
* [x] Single-player station (Season 2 and Season 3):
* [x] Onboarding
* [ ] Onboarding
* [x] Talk to Badum
* [x] Equip items
* [ ] Deploy to Fortuna
* [ ] Matchmaking and deployment
* [x] Solo
* [ ] Squad
@@ -37,15 +42,16 @@ Also known as "The Cycle: Frontier".
* [x] Inventory and loadout
* [ ] Loadout presets (Season 3)
* [x] Quests
* [ ] Job boards
* [x] Faction progression
* [ ] Season pass
* [ ] Aurum Shops
* [ ] Shops
* [ ] Daily shop
* [ ] Weekly shop
* [ ] Shop rotation
* [x] Daily login
* [x] Character appearance and emotes
* [x] Item Shops
* [x] Shops
* [x] Crafting station
* [x] Quarters
* [x] Player balance
@@ -54,17 +60,17 @@ Also known as "The Cycle: Frontier".
* [x] Vivox login
* [x] Vivox create and join channel
* [ ] Proximity voice works
* [x] Game mechanics
* [ ] Game mechanics
* [x] Can deploy through terminal
* [x] Can deploy with loadout
* [x] Can evac
* [x] Can do quests (except PvP)
* [x] Can gain/lose loot
* [x] Can use Alien Forge
* [x] Map content
* [ ] Can evac
* [ ] Can do quests
* [ ] Can gain/lose loot
* [ ] Can use Alien Forge.
* [ ] Map content
* [x] Bright Sands
* [x] Crescent Falls
* [x] Tharis Island
* [ ] Crescent Falls
* [ ] Tharis Island
## Running locally
@@ -84,29 +90,29 @@ 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. [`Prospect.Server.Api` and `LoaderPack`](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
- For Season 3 (the latest Steam game client), use Build 6.
1. [.NET Runtime 8.0](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.11) installed.
- For Season 2 game client, use the latest version.
1. [ASP.NET Core 8.0](https://aka.ms/dotnet-core-applaunch?framework=Microsoft.AspNetCore.App&framework_version=8.0.0&arch=x64&rid=win-x64&os=win10) installed.
1. The Cycle: Frontier game client:
- The latest version from [Steam](https://steamcommunity.com/app/868270).
- Season 2 client version `4623363103423775682` from SteamDB. See [download instructions below](#11-how-to-download-season-2-client-from-steamdb-using-steam-console).
- Season 2 client. See [download instructions below](#11-how-to-download-season-2-client-from-steamdb-using-steam-console).
#### 1.1 How to download Season 2 client from SteamDB using Steam console
- Any other version, even Season 1 works.
### 1.1 How to download Season 2 client from SteamDB using Steam console
> [!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. With Steam running, click <steam://nav/console> to open Steam console. Alternatively, you can press `Win+R` and enter `steam://nav/console`.
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.
@@ -119,7 +125,17 @@ Before you start, you'll need the following software downloaded and installed:
Use your favorite ZIP archiver and unzip the `Prospect.Server.Api.zip` downloaded from this repository.
### 3. Generate and import SSL certificate
### 3. Patch the `hosts` file
To be able to connect to the locally running server, you must replace the IP address of `2EA46.playfabapi.com` that the game uses. Do the following:
1. Open Notepad as Administrator.
1. Click **File** > **Open...** and select `C:\Windows\System32\drivers\etc\hosts`.
1. At the end of the file, add `127.0.0.1 2EA46.playfabapi.com` on a new line. Do not add the `#` character!
### 4. Generate and import SSL certificate
> [!IMPORTANT]
> Do not share the generated certificate! Generated certificate includes a private key that may be used to generate other certificates and compromise your security.
@@ -143,24 +159,38 @@ to successfully communicate with the local server. Do the following:
1. Click **Finish**. A **Security Warning** popup may appear, make sure it specifies `2EA46.playfabapi.com` certification authority and click **Yes**.
### 4. Extract `LoaderPack` to the game
### 5. Add `steam_appid` to the game
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
1. Open the `LoaderPack` archive.
1. Right-click in the folder > **New** > **Text Document**, and name it `steam_appid`.
1. Drag and drop the contents of the `LoaderPack` archive to the game.
1. Open `steam_appid` and enter `480`.
1. Create a shortcut for the `Prospect.Client.Loader` that you will use later to launch the game.
1. Save and close the file.
### 5. Run the server
### 6. Create a game shortcut with specified arguments
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
1. Right-click `Prospect-Win64-Shipping.exe` > **Create Shortcut**.
1. Right-click the created shortcut > **Properties**.
1. In the **Target** field, add the following parameters after the quote ` -log -steam_auth PF_TITLEID=2EA46`. Note the space after the quote.
1. Move the shortcut to desktop.
### 7. Run the server
Now you are all set! Open the folder with `Prospect.Server.Api` and run `Prospect.Server.Api.exe`. It will open a console if it runs successfully.
> [!IMPORTANT]
> Do not close the console when you run the game.
>
> If the console does not open, make sure you have [.NET Runtime 8.0](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.11) and [ASP.NET Core 8.0](https://aka.ms/dotnet-core-applaunch?framework=Microsoft.AspNetCore.App&framework_version=8.0.0&arch=x64&rid=win-x64&os=win10) installed.
### 6. Run the game
### 8. Run the game
Once the server is running, make sure that Steam is running and open The Cycle: Frontier using the shortcut you've created before.
@@ -201,14 +231,17 @@ Make sure that:
### Login Failed. Error code: 5
Make sure that `Prospect.Server.Api` server is running.
Make sure that:
If the server is running, press `Alt+Tab` to a game console that opens when you start the game and check for the following:
* `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**.
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.
![](./_assets/connection_refused_error.png)
* `InvalidAPIEndpoint` - indicates that you are running the game using the original shortcut and not using `Prospect.Client.Loader`.
* `InvalidAPIEndpoint` - indicates that the `hosts` file was not updated properly.
![](./_assets/invalid_api_endpoint.PNG)
* `libcurl error 60 (Peer certificate cannot be authenticated with given CA certificates)` - indicates that the certificate was not installed correctly. Make sure that the certificate is present in `certmgr.msc` and there is only one certificate. Try removing the certificate and importing it again by following step 4.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-119
View File
@@ -1,119 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{a9ba7d25-f239-4320-a0dc-85e8001fd669}</ProjectGuid>
<RootNamespace>ProspectAgent</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<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" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(ProjectDir)bin\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\</IntDir>
<LibraryPath>$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<LinkIncremental>false</LinkIncremental>
<OutDir>$(ProjectDir)bin\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\</IntDir>
<LibraryPath>$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<VcpkgUseStatic>true</VcpkgUseStatic>
</PropertyGroup>
<PropertyGroup Label="Vcpkg" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<VcpkgUseStatic>true</VcpkgUseStatic>
</PropertyGroup>
<PropertyGroup Label="Vcpkg">
<VcpkgEnableManifest>true</VcpkgEnableManifest>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;PROSPECTAGENT_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<Optimization>MinSpace</Optimization>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableUAC>false</EnableUAC>
<AdditionalDependencies>%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<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" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -1,39 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</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>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="logger.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>
-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();
}
-135
View File
@@ -1,135 +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"
#include <string>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
#include <locale>
std::wstring g_backendUrl;
// trim from start (in place)
inline void ltrim(std::wstring &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
}
// trim from end (in place)
inline void rtrim(std::wstring &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
}
inline void trim(std::wstring &s) {
rtrim(s);
ltrim(s);
}
constexpr uintptr_t OffsetGMalloc = 0x63EC4A0;
/**
* Function offset, find with "?sdk=".
*/
constexpr uintptr_t OffsetPlayFabApiGetUrl = 0xCEEF00;
/**
* 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());
if (!g_backendUrl.empty())
{
return g_backendUrl + std::wstring(callPath.c_str());
}
return std::wstring(L"https://127.0.0.1:8443") + 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);
}
const wchar_t* filePath = L"backend.txt";
std::wifstream f(filePath);
if (f) {
std::wstringstream buffer;
buffer << f.rdbuf();
g_backendUrl = buffer.str();
trim(g_backendUrl);
if (g_backendUrl.rfind(L"https://", 0) != 0) {
logger::Print("[Agent] URL must start with https://\n");
g_backendUrl.clear();
} else {
logger::Print("[Agent] Loaded custom URL: %S\n", g_backendUrl.c_str());
}
} else {
logger::Print("[Agent] Failed to open file: %S\n", filePath);
}
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"
]
}
-404
View File
@@ -1,404 +0,0 @@
using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Json;
namespace Prospect.Client.Config;
// Cross-platform (Linux/Proton + Windows) helper to point a The Cycle: Frontier client at a
// server. The Prospect agent reads the target PlayFab URL from `backend.txt` next to the game
// executable, so switching servers = writing that one file. This tool also imports the server
// certificate and launches the game (natively on Windows, guided on Linux/Proton).
internal static class Program
{
private const string AppId = "868270";
// Presets use the public domains so the same target works for everyone (LAN or internet):
// DNS resolves to the public IP for friends, and the unified TLS cert covers both hostnames.
// On the same LAN, add a hosts entry (domain -> 192.168.1.136) to avoid hairpin NAT.
private static readonly (string Key, string Label, string Url)[] Presets =
{
("prod", "PROD", "https://tc.nfteam.ovh:8443"),
("preprod", "PREPROD", "https://rd-tc.nfteam.ovh:8444"),
};
private static string SettingsPath => Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ProspectSwitcher", "settings.json");
private static Settings _settings = new();
private static int Main(string[] args)
{
try { Console.OutputEncoding = Encoding.UTF8; } catch { /* some terminals reject it */ }
LoadSettings();
return args.Length > 0 ? RunCli(args) : MenuLoop();
}
// ---- Scriptable CLI ----------------------------------------------------
private static int RunCli(string[] args)
{
string? folder = null, target = null;
for (var i = 0; i < args.Length; i++)
{
switch (args[i])
{
case "--folder" when i + 1 < args.Length: folder = args[++i]; break;
case "--set" when i + 1 < args.Length: target = args[++i]; break;
case "-h" or "--help": PrintCliHelp(); return 0;
}
}
if (folder != null) { _settings.GameFolder = folder; SaveSettings(); }
if (target == null) { PrintCliHelp(); return folder != null ? 0 : 1; }
var url = ResolveTarget(target);
if (url == null) { Console.Error.WriteLine("Cible inconnue : " + target); return 1; }
return WriteBackend(url) ? 0 : 1;
}
private static void PrintCliHelp()
{
Console.WriteLine("ProspectServerSwitcher — sélecteur de serveur The Cycle (multi-OS)");
Console.WriteLine();
Console.WriteLine(" (sans argument) menu interactif");
Console.WriteLine(" --folder <chemin Win64> définit le dossier du jeu (mémorisé)");
Console.WriteLine(" --set <cible> écrit backend.txt");
Console.WriteLine();
Console.WriteLine("Cibles : " + string.Join(", ", Presets.Select(p => p.Key)) + ", ou une URL https://…");
Console.WriteLine("Ex : ProspectServerSwitcher --folder \"…/Prospect/Binaries/Win64\" --set preprod-lan");
}
private static string? ResolveTarget(string t)
{
foreach (var p in Presets)
if (string.Equals(p.Key, t, StringComparison.OrdinalIgnoreCase)) return p.Url;
if (t.StartsWith("http", StringComparison.OrdinalIgnoreCase)) return t;
return null;
}
// ---- Interactive menu --------------------------------------------------
private static int MenuLoop()
{
while (true)
{
Console.WriteLine();
Console.WriteLine("=== The Cycle — Sélecteur de serveur ===");
Console.WriteLine("Dossier du jeu : " + (string.IsNullOrEmpty(_settings.GameFolder) ? "(non défini)" : _settings.GameFolder));
Console.WriteLine("backend.txt : " + ReadCurrentBackend());
Console.WriteLine();
Console.WriteLine(" 1) Définir le dossier du jeu (Win64)");
for (var i = 0; i < Presets.Length; i++)
Console.WriteLine($" {i + 2}) {Presets[i].Label,-24} {Presets[i].Url}");
var n = Presets.Length + 2;
Console.WriteLine($" {n}) Serveur personnalisé…");
Console.WriteLine($" {n + 1}) Importer un certificat…");
Console.WriteLine($" {n + 2}) Lancer le jeu");
Console.WriteLine(" 0) Quitter");
Console.Write("> ");
var raw = Console.ReadLine()?.Trim();
if (raw is null or "0") return 0;
if (!int.TryParse(raw, out var choice)) { Console.WriteLine("Choix invalide."); continue; }
if (choice == 1) SetFolder();
else if (choice >= 2 && choice < n) WriteBackend(Presets[choice - 2].Url);
else if (choice == n) SetCustom();
else if (choice == n + 1) ImportCert();
else if (choice == n + 2) LaunchGame();
else Console.WriteLine("Choix invalide.");
}
}
private static void SetFolder()
{
Console.Write("Chemin du dossier Win64 : ");
var p = Console.ReadLine()?.Trim().Trim('"');
if (string.IsNullOrEmpty(p)) return;
if (!Directory.Exists(p)) { Console.WriteLine("⚠ Dossier introuvable."); return; }
_settings.GameFolder = p;
SaveSettings();
Console.WriteLine("✔ Dossier mémorisé.");
}
private static void SetCustom()
{
Console.Write("URL du serveur (ex : https://mon-serveur:8443) : ");
var u = Console.ReadLine()?.Trim();
if (!string.IsNullOrWhiteSpace(u)) WriteBackend(u);
}
// ---- backend.txt -------------------------------------------------------
private static bool WriteBackend(string url)
{
if (!EnsureFolder(out var dir)) return false;
try
{
// No trailing newline: the agent reads the raw file and requires it to start with https://.
File.WriteAllText(Path.Combine(dir, "backend.txt"), url, new UTF8Encoding(false));
Console.WriteLine($"✔ backend.txt → {url}");
Console.WriteLine(" (relance le jeu : backend.txt n'est lu qu'au démarrage du loader.)");
return true;
}
catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return false; }
}
private static string ReadCurrentBackend()
{
if (string.IsNullOrEmpty(_settings.GameFolder)) return "(dossier non défini)";
var f = Path.Combine(_settings.GameFolder, "backend.txt");
try
{
if (!File.Exists(f)) return "(absent)";
var v = File.ReadAllText(f).Trim();
var match = Presets.FirstOrDefault(p => p.Url == v);
return match.Key != null ? $"{v} [{match.Label.Trim()}]" : v;
}
catch { return "(illisible)"; }
}
// ---- Certificate -------------------------------------------------------
private static void ImportCert()
{
// Prefer fetching the cert live from the server the client points at (backend.txt), so
// it's always the current one; fall back to a .crt file.
X509Certificate2? cert = null;
var target = ReadBackendUrl();
if (target != null && TryFetchServerCert(target, out cert))
Console.WriteLine($"Certificat récupéré depuis {target}");
else
{
Console.Write("Chemin d'un certificat (.crt) : ");
var p = Console.ReadLine()?.Trim().Trim('"');
if (string.IsNullOrEmpty(p) || !File.Exists(p)) { Console.WriteLine("⚠ Annulé."); return; }
try { cert = new X509Certificate2(p); } catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return; }
}
if (cert == null) { Console.WriteLine("⚠ Aucun certificat."); return; }
Console.WriteLine($" empreinte : {cert.Thumbprint}");
if (OperatingSystem.IsWindows()) ImportCertWindows(cert);
else ImportCertProton(cert);
}
private static string? ReadBackendUrl()
{
if (string.IsNullOrEmpty(_settings.GameFolder)) return null;
var f = Path.Combine(_settings.GameFolder, "backend.txt");
try { return File.Exists(f) ? File.ReadAllText(f).Trim() : null; } catch { return null; }
}
private static bool TryFetchServerCert(string url, out X509Certificate2? cert)
{
cert = null;
try
{
var uri = new Uri(url);
using var tcp = new System.Net.Sockets.TcpClient();
tcp.Connect(uri.Host, uri.Port <= 0 ? 443 : uri.Port);
X509Certificate2? captured = null;
using var ssl = new System.Net.Security.SslStream(tcp.GetStream(), false,
(_, c, _, _) => { if (c != null) captured = new X509Certificate2(c.Export(X509ContentType.Cert)); return true; });
ssl.AuthenticateAsClient(uri.Host);
cert = captured;
return cert != null;
}
catch { return false; }
}
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
private static void ImportCertWindows(X509Certificate2 cert)
{
try
{
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
Console.WriteLine("✔ Certificat importé (Autorités racines de confiance, utilisateur).");
}
catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); }
}
// Trust the cert inside the game's Proton/Wine prefix by writing the serialized cert blob
// into the prefix Root store via `wine reg import` (the wine `certutil` tool is unreliable
// under Proton). The prefix is chosen explicitly, so it works even though the client runs
// as a non-Steam shortcut (whose appid isn't the game's 868270).
private static void ImportCertProton(X509Certificate2 cert)
{
var wine = FindProtonWine();
if (wine == null) { Console.WriteLine("⚠ Aucun wine Proton trouvé (Steam/Proton installé ?)."); return; }
var prefix = SelectPrefix();
if (prefix == null) return;
// Wine serialized cert blob: SHA1-hash property (id 3) + encoded cert property (id 0x20).
var der = cert.RawData;
var sha1 = cert.GetCertHash();
byte[] blob;
using (var ms = new MemoryStream())
{
void Prop(uint id, byte[] data)
{
ms.Write(BitConverter.GetBytes(id));
ms.Write(BitConverter.GetBytes(1u));
ms.Write(BitConverter.GetBytes((uint)data.Length));
ms.Write(data);
}
Prop(3, sha1);
Prop(0x20, der);
blob = ms.ToArray();
}
var thumb = Convert.ToHexString(sha1);
var hex = string.Join(",", blob.Select(b => b.ToString("x2")));
var regFile = Path.Combine(Path.GetTempPath(), "prospect-cert.reg");
File.WriteAllText(regFile,
"REGEDIT4\r\n\r\n" +
$"[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SystemCertificates\\Root\\Certificates\\{thumb}]\r\n" +
$"\"Blob\"=hex:{hex}\r\n");
Console.WriteLine("⚠ Ferme complètement le jeu avant de continuer (sinon le registre ne sera pas pris en compte).");
Console.Write("Appuie sur Entrée pour importer… ");
Console.ReadLine();
var wineserver = Path.Combine(Path.GetDirectoryName(wine)!, "wineserver");
var regWinePath = "Z:" + regFile.Replace('/', '\\'); // Z: maps to / inside the prefix
RunWine(wineserver, "-k", prefix); // stop any running wineserver (flush)
RunWine(wine, $"reg import \"{regWinePath}\"", prefix);
RunWine(wineserver, "-k", prefix); // flush the change to disk
var sysreg = Path.Combine(prefix, "system.reg");
var ok = File.Exists(sysreg) && File.ReadAllText(sysreg).Contains(thumb, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(ok
? $"✔ Certificat importé dans le préfixe : {prefix}\n Relance le jeu."
: "✖ Import non confirmé — assure-toi que le jeu est fermé et réessaie.");
}
private static void RunWine(string exe, string args, string prefix)
{
try
{
var psi = new ProcessStartInfo(exe, args)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
psi.Environment["WINEPREFIX"] = prefix;
psi.Environment["WINEDEBUG"] = "-all";
using var p = Process.Start(psi);
p?.WaitForExit(20000);
}
catch (Exception ex) { Console.WriteLine(" (wine) " + ex.Message); }
}
private static string? FindProtonWine()
{
foreach (var root in SteamRoots())
{
var common = Path.Combine(root, "steamapps", "common");
if (!Directory.Exists(common)) continue;
var candidates = Directory.GetDirectories(common, "Proton*")
.Select(d => Path.Combine(d, "files", "bin", "wine"))
.Where(File.Exists)
.OrderByDescending(w => w.Contains("Experimental"))
.ThenByDescending(File.GetLastWriteTimeUtc)
.ToList();
if (candidates.Count > 0) return candidates[0];
}
return null;
}
private static string? SelectPrefix()
{
if (!string.IsNullOrEmpty(_settings.WinePrefix) && Directory.Exists(_settings.WinePrefix))
return _settings.WinePrefix;
var prefixes = new List<string>();
foreach (var root in SteamRoots())
{
var cd = Path.Combine(root, "steamapps", "compatdata");
if (!Directory.Exists(cd)) continue;
foreach (var d in Directory.GetDirectories(cd))
{
var pfx = Path.Combine(d, "pfx");
if (File.Exists(Path.Combine(pfx, "system.reg"))) prefixes.Add(Path.GetFullPath(pfx));
}
}
prefixes = prefixes.Distinct()
.OrderByDescending(p => File.GetLastWriteTimeUtc(Path.Combine(p, "system.reg")))
.ToList();
if (prefixes.Count == 0) { Console.WriteLine("⚠ Aucun préfixe Proton trouvé."); return null; }
Console.WriteLine("Préfixes Proton détectés (le plus récemment utilisé en premier = sûrement ton jeu) :");
for (var i = 0; i < prefixes.Count; i++)
Console.WriteLine($" {i + 1}) {prefixes[i]}");
Console.Write("Numéro du préfixe (Entrée = 1) : ");
var raw = Console.ReadLine()?.Trim();
var idx = string.IsNullOrEmpty(raw) ? 0 : (int.TryParse(raw, out var n) ? n - 1 : -1);
if (idx < 0 || idx >= prefixes.Count) { Console.WriteLine("⚠ Choix invalide."); return null; }
_settings.WinePrefix = prefixes[idx];
SaveSettings();
return _settings.WinePrefix;
}
private static IEnumerable<string> SteamRoots()
{
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
yield return Path.Combine(home, ".steam", "steam");
yield return Path.Combine(home, ".local", "share", "Steam");
}
// ---- Launch ------------------------------------------------------------
private static void LaunchGame()
{
if (OperatingSystem.IsWindows())
{
if (!EnsureFolder(out var dir)) return;
var loader = Path.Combine(dir, "Prospect.Client.Loader.exe");
if (!File.Exists(loader)) { Console.WriteLine("⚠ Prospect.Client.Loader.exe introuvable dans le dossier."); return; }
try
{
Process.Start(new ProcessStartInfo(loader) { WorkingDirectory = dir, UseShellExecute = true });
Console.WriteLine("▶ Jeu lancé.");
}
catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); }
return;
}
Console.WriteLine("Sous Linux/Proton, lance via Steam (le loader doit tourner dans le préfixe Proton) :");
Console.WriteLine(" steam steam://rungameid/" + AppId);
}
// ---- Helpers -----------------------------------------------------------
private static bool EnsureFolder(out string dir)
{
dir = _settings.GameFolder;
if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
{
Console.WriteLine("⚠ Définis d'abord le dossier du jeu (option 1).");
return false;
}
return true;
}
private static void LoadSettings()
{
try
{
if (File.Exists(SettingsPath))
_settings = JsonSerializer.Deserialize<Settings>(File.ReadAllText(SettingsPath)) ?? new Settings();
}
catch { _settings = new Settings(); }
}
private static void SaveSettings()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(_settings));
}
catch { /* best-effort persistence */ }
}
private sealed class Settings
{
public string GameFolder { get; set; } = "";
public string WinePrefix { get; set; } = "";
}
}
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>ProspectServerSwitcher</AssemblyName>
<RootNamespace>Prospect.Client.Config</RootNamespace>
<InvariantGlobalization>true</InvariantGlobalization>
<!-- Cross-platform client tool (Linux/Proton + Windows). Never built by the Linux server CI. -->
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
</Project>
-102
View File
@@ -1,102 +0,0 @@
# ProspectServerSwitcher
Petit utilitaire **multi-OS** (Linux/Proton + Windows) pour pointer un client
*The Cycle: Frontier* vers un serveur, importer le certificat, et lancer le jeu.
## Pourquoi
L'agent Prospect (`Prospect.Agent.dll`, injecté par `Prospect.Client.Loader.exe`)
hooke l'URL de l'API PlayFab et la remplace par le contenu de **`backend.txt`**,
placé à côté de l'exécutable du jeu (`Prospect/Binaries/Win64`). S'il est absent,
l'agent retombe sur `https://127.0.0.1:8443`.
**Tout l'aiguillage du client tient donc dans cette seule valeur.** Cet outil ne fait
essentiellement qu'écrire le bon `backend.txt` — plus quelques commodités (cert, launch).
## Serveurs (presets)
Les presets utilisent les **domaines publics** pour que la même cible marche pour
tout le monde (le DNS résout vers l'IP publique pour les amis, et le certificat TLS
unifié couvre les deux hostnames) :
| Preset | URL |
|-----------|----------------------------------|
| `prod` | `https://tc.nfteam.ovh:8443` |
| `preprod` | `https://rd-tc.nfteam.ovh:8444` |
> **Sur le même LAN que le serveur**, ajoute une entrée hosts pour éviter le hairpin NAT :
> ```
> 192.168.1.136 tc.nfteam.ovh
> 192.168.1.136 rd-tc.nfteam.ovh
> ```
## Utilisation
### Menu interactif
Lance le binaire sans argument :
- **1** : définir le dossier du jeu (`…/Prospect/Binaries/Win64`) — mémorisé
- **2/3** : basculer sur PROD / PREPROD
- **4** : URL personnalisée
- **5** : importer un certificat (`.crt`)
- **6** : lancer le jeu
- **0** : quitter
### En ligne de commande (scriptable)
```
ProspectServerSwitcher --folder "<...>/Prospect/Binaries/Win64" --set preprod
ProspectServerSwitcher --set prod
ProspectServerSwitcher --set https://mon-serveur:8443 # URL libre
```
> ⚠️ `backend.txt` n'est lu qu'**au démarrage** du loader → relance le jeu après un switch.
> Le serveur ciblé doit **tourner** (conteneurs on-demand `the-cycle-api` / `the-cycle-api-rd`).
## Certificat
L'outil récupère le certificat **en direct depuis le serveur** ciblé (il lit `backend.txt`
et se connecte en TLS), puis le rend fiable :
- **Windows** : import automatique dans *Autorités de certification racines de confiance*
(utilisateur courant), via le magasin `X509Store`.
- **Linux/Proton** : import **automatique dans le préfixe Wine** du jeu. Comme `wine certutil`
est cassé sous Proton, l'outil écrit directement le *blob* sérialisé du certificat
(propriété SHA1 `id=3` + certificat encodé `id=0x20`) dans le magasin `Root` du préfixe via
`wine reg import`. Il :
1. détecte le binaire `wine` de Proton (`…/common/Proton*/files/bin/wine`),
2. liste les préfixes `compatdata/*/pfx` (le plus récemment utilisé en premier),
3. importe dans le préfixe choisi (⚠️ **le jeu doit être fermé**), mémorisé ensuite.
> ⚠️ Le client tourne souvent comme **raccourci non-Steam** → son préfixe n'est **pas**
> `compatdata/868270` mais un appid généré (ex. `3883998305`). D'où la sélection explicite
> du préfixe plutôt qu'une déduction sur l'appid du jeu.
## Lancement
- **Windows** : démarre `Prospect.Client.Loader.exe` depuis le dossier du jeu.
- **Linux/Proton** : passe par Steam (`steam steam://rungameid/868270`) — le loader doit
tourner dans le préfixe Proton.
## Réglages
Le dossier du jeu est mémorisé dans
`…/ApplicationData/ProspectSwitcher/settings.json`
(`%AppData%` sous Windows, `~/.config` sous Linux).
## Build / publication
Projet console **net8.0** (cross-platform, `InvariantGlobalization`), jamais buildé par
la CI serveur (Linux).
```bash
# build simple
dotnet build src/Prospect.Client.Config -c Release
# binaires autonomes single-file (aucun .NET requis chez l'utilisateur)
dotnet publish src/Prospect.Client.Config -c Release -r win-x64 --self-contained \
-p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:EnableCompressionInSingleFile=true
dotnet publish src/Prospect.Client.Config -c Release -r linux-x64 --self-contained \
-p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true
```
Les binaires publiés sont distribués via la release Gitea **`friends-client`**
(`ProspectServerSwitcher.exe` pour Windows, `ProspectServerSwitcher-linux-x64` pour Linux).
@@ -1,138 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<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">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>17.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{04b1d963-2bcb-4887-9d6d-04cb3366a348}</ProjectGuid>
<RootNamespace>ProspectClientLoader</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<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">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v143</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</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'">
<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|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<OutDir>$(SolutionDir)\$(ProjectName)\$(Platform)\$(Configuration)\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="main.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>
-500
View File
@@ -1,500 +0,0 @@
#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <stdint.h>
#include <string>
std::wstring ExePath() {
TCHAR buffer[MAX_PATH] = { 0 };
GetModuleFileName( NULL, buffer, MAX_PATH );
std::wstring::size_type pos = std::wstring(buffer).find_last_of(L"\\/");
return std::wstring(buffer).substr(0, pos);
}
// 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 {
wchar_t szErrorMessage[512];
DWORD dwErrorCode = GetLastError();
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dwErrorCode,
0,
szErrorMessage,
sizeof(szErrorMessage),
NULL
);
printf("Failed to start process. Error: %lu: %ws\n", dwErrorCode, szErrorMessage);
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 WriteBytes(HANDLE hProcess, uintptr_t address, BYTE* newValues, size_t size) {
SIZE_T bytesWritten;
DWORD oldProtect;
if (VirtualProtectEx(hProcess, (LPVOID)address, size, PAGE_EXECUTE_READWRITE, &oldProtect)) {
if (WriteProcessMemory(hProcess, (LPVOID)address, newValues, size, &bytesWritten)) {
printf("Memory Write Success at address 0x%p\n", (void*)address);
}
else {
printf("Failed to write memory. Error: %lu\n", GetLastError());
return false;
}
// Restore original protection
VirtualProtectEx(hProcess, (LPVOID)address, size, oldProtect, &oldProtect);
}
else {
printf("Failed to change memory protection. Error: %lu\n", GetLastError());
return false;
}
return true;
}
bool CheckForceLevelStreamingOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
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 CheckForceLevelStreamingOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool CheckGPStringOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0x5F && buffer[1] == 0x00;
}
else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckForceLevelStreamingOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool CheckWeakPointerCheckJumpOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0xF8 && buffer[1] == 0xFE;
}
else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckWeakPointerCheckJumpOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool CheckWeakPointerCheckOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[3];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0x0F && buffer[1] == 0x1F && buffer[2] == 0x40;
}
else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckWeakPointerCheckOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool PatchForceLevelStreaming(HANDLE hProcess, uintptr_t imageBase) {
// Season 3 Patch force disable level streaming: sub_3DA7520 -> loc_3DA75B6
uintptr_t address = imageBase + 0x3DA75B6;
bool found = CheckForceLevelStreamingOffset(hProcess, address);
if (!found) {
// Season 2 Patch force disable level streaming: sub_3D19900 -> loc_3D19996
address = imageBase + 0x3D19996;
found = CheckForceLevelStreamingOffset(hProcess, address);
}
if (!found) {
printf("Failed to patch force level streaming!\n");
return false;
}
{
BYTE newValues[2] = { 0xB3, 0x01 };
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
}
address = imageBase + 0x5541C08;
found = CheckGPStringOffset(hProcess, address);
if (!found) {
address = imageBase + 0x54E0558;
found = CheckGPStringOffset(hProcess, address);
}
if (!found) {
printf("Failed to find _GP string!\n");
return false;
}
{
BYTE newValues[6] = { 0x47, 0x00, 0x00, 0x00, 0x00, 0x00 };
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
}
address = imageBase + 0x16D7C24;
found = CheckWeakPointerCheckOffset(hProcess, address);
// TODO
//if (!found) {
// address = imageBase + 0x54E0558;
// found = CheckWeakPointerCheckOffset(hProcess, address);
//}
if (!found) {
printf("Failed to find Weak Pointer Check!\n");
return false;
}
{
BYTE newValues[28] = {
0x48, 0x8B, 0x06,
0x48, 0x8D, 0x4D, 0x7F,
0x48, 0x89, 0x45, 0x7F,
0xE8, 0xDC, 0xC7, 0xA7, 0x00,
0x48, 0x85, 0xC0,
0x0F, 0x84, 0xEE, 0x00, 0x00, 0x00,
0x90, 0x90, 0x90
};
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
}
address = imageBase + 0x16D7D34;
found = CheckWeakPointerCheckJumpOffset(hProcess, address);
// TODO
//if (!found) {
// address = imageBase + 0x54E0558;
// found = CheckWeakPointerCheckJumpOffset(hProcess, address);
//}
if (!found) {
printf("Failed to find Weak Pointer Check Jump!\n");
return false;
}
{
BYTE newValues[1] = { 0xEC };
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
}
return true;
}
bool CheckUpdateInventoryFunctionOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0x84 && buffer[1] == 0xC0;
}
else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckUpdateInventoryFunctionOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool PatchUpdateInventory(HANDLE hProcess, uintptr_t imageBase) {
uintptr_t address = imageBase + 0x18D5A11;
bool found = CheckUpdateInventoryFunctionOffset(hProcess, address);
if (!found) {
address = imageBase + 0x3D19B19;
found = CheckUpdateInventoryFunctionOffset(hProcess, address);
}
if (!found) {
printf("Failed to patch inventory update function!\n");
return false;
}
BYTE newValues[8] = { 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
return true;
}
bool CheckSetupPlayerContractsOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0x48 && buffer[1] == 0x8D;
}
else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckSetupPlayerContractsOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool CheckUpdatePlayerContractsOffset(HANDLE hProcess, uintptr_t address) {
BYTE buffer[2];
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, (LPCVOID)address, buffer, sizeof(buffer), &bytesRead)) {
return buffer[0] == 0x3B && buffer[1] == 0xC1;
}
else {
DWORD error = GetLastError();
if (error == 299) {
printf("Incomplete memory read. Retrying...\n");
Sleep(1000);
return CheckUpdatePlayerContractsOffset(hProcess, address);
}
printf("Failed to read memory. Error: %lu\n", error);
return false;
}
}
bool PatchLoadPlayerContracts(HANDLE hProcess, uintptr_t imageBase) {
uintptr_t address = imageBase + 0x171E9EC;
bool found = CheckUpdatePlayerContractsOffset(hProcess, address);
if (!found) {
printf("Failed to update player contracts function!\n");
return false;
}
{
BYTE newValues[4] = { 0x90, 0x90, 0xEB, 0x21 };
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
}
address = imageBase + 0x1706127;
found = CheckSetupPlayerContractsOffset(hProcess, address);
if (!found) {
printf("Failed to find setup player contracts function!\n");
return false;
}
{
BYTE newValues[39] = {
0x45, 0x33, 0xC0, 0x49, 0x8B, 0xD5, 0x48, 0x8B, 0x89, 0xD8, 0x00, 0x00, 0x00, 0xE8, 0x47,
0xE2, 0x10, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xEB
};
bool result = WriteBytes(hProcess, address, newValues, sizeof(newValues));
if (!result) {
return false;
}
}
return true;
}
bool Inject(HANDLE hProcess, const wchar_t* dllPath) {
// Allocate space in the target process for the DLL path
size_t pathSize = (wcslen(dllPath) + 1) * sizeof(wchar_t);
LPVOID remotePath = VirtualAllocEx(hProcess, NULL, pathSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!remotePath) {
wprintf(L"Failed to allocate memory in remote process. Error: %lu\n", GetLastError());
CloseHandle(hProcess);
return false;
}
// Write the DLL path into the target process
if (!WriteProcessMemory(hProcess, remotePath, dllPath, pathSize, NULL)) {
wprintf(L"Failed to write memory. Error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
CloseHandle(hProcess);
return false;
}
// Get the address of LoadLibraryW in kernel32.dll
LPVOID loadLibraryAddr = (LPVOID)GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
if (!loadLibraryAddr) {
wprintf(L"Failed to get LoadLibraryW address. Error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
CloseHandle(hProcess);
return false;
}
// Create a remote thread to call LoadLibraryW with our DLL path
HANDLE hThread = CreateRemoteThread(
hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)loadLibraryAddr,
remotePath, 0, NULL);
if (!hThread) {
wprintf(L"Failed to create remote thread. Error: %lu\n", GetLastError());
VirtualFreeEx(hProcess, remotePath, 0, MEM_RELEASE);
CloseHandle(hProcess);
return false;
}
return true;
}
bool Patch(const wchar_t* processName) {
DWORD processID;
HANDLE hProcess = StartProcess(processName, &processID);
if (hProcess == NULL) {
return false;
}
if (!Inject(hProcess, L"Prospect.Agent.dll")) {
return false;
}
if (!Inject(hProcess, L"UE4SS.dll")) {
return false;
}
uintptr_t imageBase;
// Wait a bit for PE image to load
Sleep(250);
while ((imageBase = GetModuleBaseAddress(processID)) == 0) {
printf("Failed to find PE Image Base Address. Retrying...\n");
Sleep(250);
}
printf("PE Image Base Address: 0x%p\n", (void*)imageBase);
bool success = !PatchForceLevelStreaming(hProcess, imageBase) || !PatchUpdateInventory(hProcess, imageBase) || !PatchLoadPlayerContracts(hProcess, imageBase);
if (success) {
return false;
}
// Clean up
CloseHandle(hProcess);
return true;
}
bool WriteSteamAppIDFile() {
const char* filename = "steam_appid.txt";
FILE* fp;
if (fopen_s(&fp, filename, "r") == 0) {
// File exists, close the file
fclose(fp);
printf("File '%s' already exists.\n", filename);
return true;
}
else {
// File does not exist, create and write to it
if (fopen_s(&fp, filename, "w") == 0) {
const char* content = "480";
size_t written = fwrite(content, sizeof(char), strlen(content), fp);
fclose(fp);
printf("File '%s' was created and written successfully.\n", filename);
}
else {
printf("Error creating the file '%s'.\n", filename);
return false;
}
}
return true;
}
int main() {
printf("Current working directory: %ws\n", ExePath().c_str());
bool success = WriteSteamAppIDFile();
if (!success) {
// Wait for user input to exit
printf("An error occurred when writing steam_appid.txt! Press any key to exit...\n");
getchar();
return 1;
}
success = Patch(L"Prospect-Win64-Shipping.exe");
if (!success) {
// 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;
}
@@ -4,6 +4,4 @@ public class PlayFabSettings
{
public string PublisherId { get; set; }
public string TitleId { get; set; }
public string SignalRURL { get; set; }
public string SignalRAccessToken { get; set; }
}
@@ -1,328 +0,0 @@
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Prospect.Server.Api.Services.Database;
using Prospect.Server.Api.Services.Database.Models;
using Prospect.Server.Api.Services.Steam;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Controllers;
// Read-only back-office to visualise everything relevant in the database.
// Served at /admin, restricted to LAN / loopback callers (an admin panel must not be
// reachable from the public internet even though :8443 is port-forwarded).
[Route("admin")]
public class AdminController : ControllerBase
{
private readonly DbUserService _userService;
private readonly DbEntityService _entityService;
private readonly DbUserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly SteamWebApiService _steamWebApi;
public AdminController(DbUserService userService, DbEntityService entityService,
DbUserDataService userDataService, TitleDataService titleDataService, SteamWebApiService steamWebApi)
{
_userService = userService;
_entityService = entityService;
_userDataService = userDataService;
_titleDataService = titleDataService;
_steamWebApi = steamWebApi;
}
// ---------- pages ----------
[HttpGet("")]
public async Task<IActionResult> Index()
{
if (!IsLan()) return Denied();
var users = await _userService.GetAllAsync();
var entities = await _entityService.GetAllAsync();
var dataCount = await _userDataService.CountAsync();
var titleKeys = _titleDataService.Find(new List<string>());
var sb = new StringBuilder();
sb.Append("<div class='cards'>");
sb.Append(Card("Joueurs", users.Count.ToString()));
sb.Append(Card("Entités (title_player)", entities.Count.ToString()));
sb.Append(Card("Docs UserData", dataCount.ToString()));
sb.Append(Card("Clés TitleData", titleKeys.Count.ToString()));
sb.Append("</div>");
sb.Append("<h2>Joueurs récents</h2>");
sb.Append("<table><thead><tr><th>DisplayName</th><th>PlayFab Id</th><th>Auth</th><th></th></tr></thead><tbody>");
foreach (var u in users.Take(15))
{
var auth = u.Auth != null ? string.Join(", ", u.Auth.Select(a => $"{a.Type}:{a.Key}")) : "";
sb.Append($"<tr><td>{Esc(u.DisplayName)}</td><td class='mono'>{Esc(u.Id)}</td><td class='mono small'>{Esc(auth)}</td>" +
$"<td><a class='btn' href='/admin/players/{Esc(u.Id)}'>Voir</a></td></tr>");
}
sb.Append("</tbody></table>");
sb.Append("<p><a class='btn' href='/admin/players'>Tous les joueurs →</a> &nbsp; <a class='btn' href='/admin/titledata'>TitleData →</a></p>");
return Html(Layout("Dashboard", sb.ToString()));
}
[HttpGet("players")]
public async Task<IActionResult> Players([FromQuery] string? q)
{
if (!IsLan()) return Denied();
var users = await _userService.GetAllAsync();
var entities = (await _entityService.GetAllAsync())
.GroupBy(e => e.UserId)
.ToDictionary(g => g.Key, g => g.First().Id);
if (!string.IsNullOrWhiteSpace(q))
{
var needle = q.Trim().ToLowerInvariant();
users = users.Where(u =>
(u.DisplayName ?? "").ToLowerInvariant().Contains(needle) ||
(u.Id ?? "").ToLowerInvariant().Contains(needle) ||
(u.Auth != null && u.Auth.Any(a => (a.Key ?? "").ToLowerInvariant().Contains(needle)))).ToList();
}
var sb = new StringBuilder();
sb.Append($"<form method='get' class='search'><input name='q' placeholder='Rechercher nom / id / steam…' value='{Esc(q ?? "")}'/><button class='btn'>Rechercher</button>" +
"<a class='btn' href='/admin/backfill-names' title='Récupère les pseudos Steam des comptes sans nom'>Backfill pseudos Steam</a></form>");
sb.Append($"<p class='muted'>{users.Count} joueur(s)</p>");
sb.Append("<table><thead><tr><th>DisplayName</th><th>PlayFab Id</th><th>Entity Id</th><th>Auth</th><th></th></tr></thead><tbody>");
foreach (var u in users.OrderBy(u => u.DisplayName))
{
var auth = u.Auth != null ? string.Join(", ", u.Auth.Select(a => $"{a.Type}:{a.Key}")) : "";
var ent = entities.TryGetValue(u.Id, out var eid) ? eid : "";
sb.Append($"<tr><td>{Esc(u.DisplayName)}</td><td class='mono'>{Esc(u.Id)}</td><td class='mono small'>{Esc(ent)}</td>" +
$"<td class='mono small'>{Esc(auth)}</td><td><a class='btn' href='/admin/players/{Esc(u.Id)}'>Voir</a></td></tr>");
}
sb.Append("</tbody></table>");
return Html(Layout("Joueurs", sb.ToString()));
}
[HttpGet("players/{id}")]
public async Task<IActionResult> Player(string id)
{
if (!IsLan()) return Denied();
var user = await _userService.FindByIdAsync(id);
if (user == null) return Html(Layout("Joueur introuvable", "<p>Aucun joueur avec cet id.</p>"));
var entity = await _entityService.FindAsync(id);
var data = await _userDataService.GetAllForUserAsync(id);
var map = data.ToDictionary(d => d.Key, d => d);
var sb = new StringBuilder();
var auth = user.Auth != null ? string.Join(", ", user.Auth.Select(a => $"{a.Type}:{a.Key}")) : "";
sb.Append($"<h2>{Esc(user.DisplayName)}</h2>");
sb.Append($"<p class='muted mono'>PlayFabId {Esc(user.Id)} · Entity {Esc(entity?.Id ?? "-")} · {Esc(auth)}</p>");
// Summary cards
sb.Append("<div class='cards'>");
if (map.TryGetValue("Balance", out var bal))
{
sb.Append(Card("K-Marks", GetNum(bal.Value, "SoftCurrency").ToString()));
sb.Append(Card("Aurum", GetNum(bal.Value, "HardCurrency").ToString()));
sb.Append(Card("Insurance", GetNum(bal.Value, "InsuranceCurrency").ToString()));
}
foreach (var f in new[] { "Korolev", "Osiris", "ICA" })
if (map.TryGetValue("FactionProgression" + f, out var fp))
sb.Append(Card("Rép. " + f, (fp.Value ?? "0").Trim()));
foreach (var s in new[] { "2", "3" })
if (map.TryGetValue($"FortunaPass{s}_SeasonXp", out var xp) && (xp.Value ?? "0").Trim() != "0")
sb.Append(Card($"Fortuna S{s} XP", (xp.Value ?? "0").Trim()));
sb.Append("</div>");
// Inventory table
if (map.TryGetValue("Inventory", out var inv))
{
sb.Append("<h3>Inventaire</h3>");
sb.Append(InventoryTable(inv.Value));
}
// All keys
sb.Append("<h3>Toutes les données</h3>");
foreach (var d in data)
{
sb.Append("<details><summary>" + Esc(d.Key) +
$" <span class='muted small'>· {d.Value?.Length ?? 0} car · maj {d.LastUpdated:yyyy-MM-dd HH:mm}{(d.Public ? " · public" : "")}</span></summary>");
sb.Append($"<pre>{Esc(Pretty(d.Value))}</pre></details>");
}
return Html(Layout("Joueur · " + user.DisplayName, sb.ToString()));
}
[HttpGet("titledata")]
public IActionResult TitleData()
{
if (!IsLan()) return Denied();
var all = _titleDataService.Find(new List<string>());
var sb = new StringBuilder();
sb.Append("<table><thead><tr><th>Clé</th><th>Taille</th><th></th></tr></thead><tbody>");
foreach (var kv in all.OrderBy(k => k.Key))
sb.Append($"<tr><td class='mono'>{Esc(kv.Key)}</td><td>{kv.Value?.Length ?? 0} car</td>" +
$"<td><a class='btn' href='/admin/titledata/{Esc(kv.Key)}'>Voir</a></td></tr>");
sb.Append("</tbody></table>");
return Html(Layout("TitleData", sb.ToString()));
}
[HttpGet("titledata/{key}")]
public IActionResult TitleDataKey(string key)
{
if (!IsLan()) return Denied();
var all = _titleDataService.Find(new List<string> { key });
if (!all.TryGetValue(key, out var value))
return Html(Layout("Clé introuvable", "<p>Aucune clé TitleData avec ce nom.</p>"));
// Large values (Blueprints, Contracts…) can be huge — pretty-print only if reasonable.
var body = value != null && value.Length > 300_000
? $"<p class='muted'>Valeur volumineuse ({value.Length} car) — affichage brut.</p><pre>{Esc(value)}</pre>"
: $"<pre>{Esc(Pretty(value))}</pre>";
return Html(Layout("TitleData · " + key, $"<h2 class='mono'>{Esc(key)}</h2>" + body));
}
[HttpGet("backfill-names")]
public async Task<IActionResult> BackfillNames()
{
if (!IsLan()) return Denied();
if (!_steamWebApi.Enabled)
return Html(Layout("Backfill", "<p class='muted'>Steam Web API non configurée (clé absente).</p><p><a class='btn' href='/admin/players'>← Joueurs</a></p>"));
var users = await _userService.GetAllAsync();
int updated = 0, skipped = 0, failed = 0;
var rows = new StringBuilder();
foreach (var u in users)
{
if (!string.IsNullOrEmpty(u.DisplayName) && u.DisplayName != "Unknown") { skipped++; continue; }
var steam = u.Auth?.FirstOrDefault(a => a.Type == PlayFabUserAuthType.Steam)?.Key;
if (string.IsNullOrEmpty(steam)) { skipped++; continue; }
var persona = await _steamWebApi.GetPersonaNameAsync(steam);
if (!string.IsNullOrWhiteSpace(persona))
{
await _userService.UpdateDisplayNameAsync(u.Id, persona);
updated++;
rows.Append($"<tr><td class='mono small'>{Esc(u.Id)}</td><td>{Esc(persona)}</td></tr>");
}
else { failed++; }
}
var body = $"<h2>Backfill des pseudos Steam</h2><p class='muted'>{updated} mis à jour · {skipped} ignorés · {failed} échec(s)</p>" +
(updated > 0 ? $"<table><thead><tr><th>PlayFabId</th><th>Nouveau nom</th></tr></thead><tbody>{rows}</tbody></table>" : "") +
"<p><a class='btn' href='/admin/players'>← Joueurs</a></p>";
return Html(Layout("Backfill", body));
}
// ---------- helpers ----------
private bool IsLan()
{
var ip = HttpContext.Connection.RemoteIpAddress;
if (ip == null) return false;
if (IPAddress.IsLoopback(ip)) return true;
if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4();
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
var b = ip.GetAddressBytes();
if (b[0] == 10) return true; // 10.0.0.0/8
if (b[0] == 192 && b[1] == 168) return true; // 192.168.0.0/16
if (b[0] == 172 && b[1] >= 16 && b[1] <= 31) return true; // 172.16.0.0/12
if (b[0] == 127) return true;
return false;
}
// IPv6 unique-local (fc00::/7) or link-local (fe80::/10)
var s = ip.GetAddressBytes();
if ((s[0] & 0xFE) == 0xFC) return true;
if (s[0] == 0xFE && (s[1] & 0xC0) == 0x80) return true;
return false;
}
private IActionResult Denied() =>
StatusCode(403, "Admin back-office is LAN-only. Access it from the local network (e.g. https://192.168.1.136:8443/admin).");
private IActionResult Html(string html) => Content(html, "text/html; charset=utf-8");
private static string Esc(string? s) => WebUtility.HtmlEncode(s ?? "");
private static string Pretty(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return json ?? "";
try
{
using var doc = JsonDocument.Parse(json);
return JsonSerializer.Serialize(doc.RootElement, new JsonSerializerOptions { WriteIndented = true });
}
catch { return json; }
}
private static long GetNum(string? json, string prop)
{
if (string.IsNullOrWhiteSpace(json)) return 0;
try
{
using var doc = JsonDocument.Parse(json);
foreach (var p in doc.RootElement.EnumerateObject())
if (string.Equals(p.Name, prop, StringComparison.OrdinalIgnoreCase) && p.Value.ValueKind == JsonValueKind.Number)
return p.Value.GetInt64();
}
catch { }
return 0;
}
private static string InventoryTable(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return "<p class='muted'>vide</p>";
try
{
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.ValueKind != JsonValueKind.Array) return $"<pre>{Esc(Pretty(json))}</pre>";
var sb = new StringBuilder();
var count = doc.RootElement.GetArrayLength();
sb.Append($"<p class='muted'>{count} item(s)</p>");
sb.Append("<table><thead><tr><th>baseItemId</th><th>amount</th><th>durability</th><th>insurance</th><th>itemId</th></tr></thead><tbody>");
foreach (var it in doc.RootElement.EnumerateArray())
{
string S(string k) => it.TryGetProperty(k, out var v) ? (v.ValueKind == JsonValueKind.Number ? v.GetRawText() : v.ToString()) : "";
sb.Append($"<tr><td>{Esc(S("baseItemId"))}</td><td>{Esc(S("amount"))}</td><td>{Esc(S("durability"))}</td>" +
$"<td>{Esc(S("insurance"))}</td><td class='mono small'>{Esc(S("itemId"))}</td></tr>");
}
sb.Append("</tbody></table>");
return sb.ToString();
}
catch { return $"<pre>{Esc(Pretty(json))}</pre>"; }
}
private static string Card(string label, string value) =>
$"<div class='card'><div class='v'>{Esc(value)}</div><div class='l'>{Esc(label)}</div></div>";
private static string Layout(string title, string body) => $@"<!doctype html><html lang='fr'><head>
<meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>
<title>{Esc(title)} · The Cycle Admin</title>
<style>
:root{{--bg:#0f1115;--panel:#171a21;--line:#262b36;--txt:#e6e8ec;--muted:#8b93a1;--acc:#e8a838;}}
*{{box-sizing:border-box}}body{{margin:0;background:var(--bg);color:var(--txt);font:14px/1.5 system-ui,Segoe UI,Roboto,sans-serif}}
header{{background:var(--panel);border-bottom:1px solid var(--line);padding:12px 20px;display:flex;gap:18px;align-items:center;position:sticky;top:0}}
header b{{color:var(--acc)}}header a{{color:var(--muted);text-decoration:none}}header a:hover{{color:var(--txt)}}
main{{padding:20px;max-width:1200px;margin:0 auto}}
h2{{margin:18px 0 10px}}h3{{margin:22px 0 8px;border-bottom:1px solid var(--line);padding-bottom:4px}}
.cards{{display:flex;flex-wrap:wrap;gap:12px;margin:12px 0}}
.card{{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:14px 18px;min-width:130px}}
.card .v{{font-size:22px;font-weight:700;color:var(--acc)}}.card .l{{color:var(--muted);font-size:12px}}
table{{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--line);border-radius:10px;overflow:hidden}}
th,td{{text-align:left;padding:8px 12px;border-bottom:1px solid var(--line);vertical-align:top}}
th{{background:#1c2029;color:var(--muted);font-weight:600}}tr:last-child td{{border-bottom:none}}
.mono{{font-family:ui-monospace,Consolas,monospace}}.small{{font-size:12px}}.muted{{color:var(--muted)}}
.btn{{display:inline-block;background:#222834;color:var(--txt);border:1px solid var(--line);border-radius:8px;padding:5px 12px;text-decoration:none;font-size:13px}}
.btn:hover{{border-color:var(--acc);color:var(--acc)}}
.search{{display:flex;gap:8px;margin:12px 0}}.search input{{flex:1;background:var(--panel);border:1px solid var(--line);border-radius:8px;color:var(--txt);padding:8px 12px}}
details{{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin:6px 0}}summary{{cursor:pointer;padding:8px 12px}}
pre{{margin:0;padding:12px;background:#0b0d11;border-top:1px solid var(--line);overflow:auto;max-height:480px;white-space:pre-wrap;word-break:break-word}}
</style></head><body>
<header><b>The Cycle · Admin</b>
<a href='/admin'>Dashboard</a><a href='/admin/players'>Joueurs</a><a href='/admin/titledata'>TitleData</a>
<span class='muted' style='margin-left:auto'>read-only · LAN</span></header>
<main>{body}</main></body></html>";
}
@@ -6,7 +6,6 @@ using Microsoft.Extensions.Options;
using Prospect.Server.Api.Config;
using Prospect.Server.Api.Models.Client;
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.Extensions;
using Prospect.Server.Api.Services.Auth.User;
@@ -32,7 +31,6 @@ public class ClientController : Controller
private readonly DbEntityService _entityService;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly Prospect.Server.Api.Services.Steam.SteamWebApiService _steamWebApi;
public ClientController(ILogger<ClientController> logger,
IOptions<PlayFabSettings> settings,
@@ -40,8 +38,7 @@ public class ClientController : Controller
DbUserService userService,
DbEntityService entityService,
UserDataService userDataService,
TitleDataService titleDataService,
Prospect.Server.Api.Services.Steam.SteamWebApiService steamWebApi)
TitleDataService titleDataService)
{
_settings = settings.Value;
_logger = logger;
@@ -50,7 +47,6 @@ public class ClientController : Controller
_entityService = entityService;
_userDataService = userDataService;
_titleDataService = titleDataService;
_steamWebApi = steamWebApi;
}
[AllowAnonymous]
@@ -66,19 +62,6 @@ public class ClientController : Controller
var userSteamId = ticket.SteamId.ToString();
var user = await _userService.FindOrCreateAsync(PlayFabUserAuthType.Steam, userSteamId);
// Backfill the display name from the Steam persona when we don't have a real
// one yet (accounts are created as "Unknown"; the client never sends a name).
if (string.IsNullOrEmpty(user.DisplayName) || user.DisplayName == "Unknown")
{
var persona = await _steamWebApi.GetPersonaNameAsync(userSteamId);
if (!string.IsNullOrWhiteSpace(persona))
{
await _userService.UpdateDisplayNameAsync(user.Id, persona);
user.DisplayName = persona;
}
}
var entity = await _entityService.FindOrCreateAsync(user.Id);
var userTicket = _authTokenService.GenerateUser(entity);
@@ -156,18 +139,8 @@ public class ClientController : Controller
[HttpPost("AddGenericID")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public async Task<IActionResult> AddGenericId(FAddGenericIDRequest request)
public IActionResult AddGenericId(FAddGenericIDRequest request)
{
// Persist the linked generic service id instead of dropping it (was a no-op stub).
var userId = User.FindAuthUserId();
if (request.GenericId != null && !string.IsNullOrEmpty(request.GenericId.ServiceName))
{
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>
{
["GenericId_" + request.GenericId.ServiceName] = request.GenericId.UserId ?? ""
});
}
return Ok(new ClientResponse<object>
{
Code = 200,
@@ -181,131 +154,19 @@ public class ClientController : Controller
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetStoreItems(FGetStoreItems request)
{
// The vendors (Korolev / ICA / Osiris / QuickShop) and other stores are defined
// inside the Blueprints title data: each item lists a per-store entry under
// ItemShopsCraftingData keyed by the store id, with the currency price in its
// recipe ingredients. This endpoint was previously a stub returning {}, which is
// why every shop showed up empty. We build the store on the fly from that data.
var storeId = request.StoreId ?? "";
var store = new List<FStoreItem>();
try
{
var titleData = _titleDataService.Find(new List<string> { "Blueprints" });
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
if (blueprints != null && !string.IsNullOrEmpty(storeId))
{
uint position = 0;
foreach (var (itemId, blueprint) in blueprints)
{
if (blueprint.ItemShopsCraftingData == null) continue;
if (!blueprint.ItemShopsCraftingData.TryGetValue(storeId, out var shopData)) continue;
if (shopData?.ItemRecipeIngredients == null) continue;
// Only currency ingredients form a purchasable price. Entries whose
// recipe is item-based (crafting) are handled by the crafting UI from
// the blueprint data the client already has, not by the buy store.
var prices = new Dictionary<string, uint>();
foreach (var ingredient in shopData.ItemRecipeIngredients)
{
var code = ingredient.Currency switch
{
"SoftCurrency" => "SC",
"Aurum" => "AU",
"InsuranceCurrency" => "IN",
_ => null,
};
if (code == null || ingredient.Amount < 0) continue;
prices[code] = prices.TryGetValue(code, out var current)
? current + (uint)ingredient.Amount
: (uint)ingredient.Amount;
}
if (prices.Count == 0) continue;
store.Add(new FStoreItem
{
ItemId = itemId,
VirtualCurrencyPrices = prices,
RealCurrencyPrices = new Dictionary<string, uint>(),
DisplayPosition = position++,
});
}
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to build store items for store {StoreId}", storeId);
}
// Cosmetic / premium stores are backed by the Vanities title data (not Blueprints):
// each vanity carries its price in StoreData.Amount, paid in Aurum. AurumShop lists
// the whole priced catalogue; the daily/weekly rotations (ids from RequestStore
// RotationData) show a deterministic slice so they change over time but stay stable
// within a period.
var isAurumShop = storeId == "AurumShop";
var isDailyShop = storeId.StartsWith("DailyShop_");
var isWeeklyShop = storeId.StartsWith("WeeklyShop_");
if (store.Count == 0 && (isAurumShop || isDailyShop || isWeeklyShop))
{
try
{
var vanityData = _titleDataService.Find(new List<string> { "Vanities" });
var vanities = JsonSerializer.Deserialize<List<TitleDataVanityInfo>>(vanityData["Vanities"]);
var priced = vanities?
.Where(v => v.StoreData != null && v.StoreData.Amount > 0)
.OrderBy(v => v.Name, StringComparer.Ordinal)
.ToList() ?? new List<TitleDataVanityInfo>();
IEnumerable<TitleDataVanityInfo> selection = priced;
if ((isDailyShop || isWeeklyShop) && priced.Count > 0)
{
var size = isWeeklyShop ? 12 : 6;
int.TryParse(storeId[(storeId.IndexOf('_') + 1)..], out var rotation);
var start = (int)(((long)rotation * size) % priced.Count);
selection = Enumerable.Range(0, Math.Min(size, priced.Count))
.Select(i => priced[(start + i) % priced.Count]);
}
uint position = 0;
foreach (var vanity in selection)
{
store.Add(new FStoreItem
{
ItemId = vanity.Name,
VirtualCurrencyPrices = new Dictionary<string, uint> { ["AU"] = (uint)vanity.StoreData.Amount },
RealCurrencyPrices = new Dictionary<string, uint>(),
DisplayPosition = position++,
});
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Failed to build vanity store {StoreId}", storeId);
}
}
_logger.LogInformation("GetStoreItems '{StoreId}': {Count} item(s)", storeId, store.Count);
return Ok(new ClientResponse<FGetStoreItemsResult>
return Ok(new ClientResponse<object>
{
Code = 200,
Status = "OK",
Data = new FGetStoreItemsResult
{
StoreId = string.IsNullOrEmpty(storeId) ? null : storeId,
CatalogVersion = "StaticItems",
Store = store,
}
Data = new {}
});
}
[HttpPost("UpdateUserTitleDisplayName")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public async Task<IActionResult> UpdateUserTitleDisplayName(FUpdateUserTitleDisplayNameRequest request)
public IActionResult UpdateUserTitleDisplayName(FUpdateUserTitleDisplayNameRequest request)
{
// Persist the rename, otherwise it is echoed back but lost on next login.
var userId = User.FindAuthUserId();
await _userService.UpdateDisplayNameAsync(userId, request.DisplayName);
return Ok(new ClientResponse<FUpdateUserTitleDisplayNameResult>
{
Code = 200,
@@ -382,15 +243,13 @@ public class ClientController : Controller
public async Task<IActionResult> GetUserInventory(FGetUserInventoryRequest request)
{
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems", "CharacterVanity", "GlobalVanity"});
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems"});
var inventory = JsonSerializer.Deserialize<FYCustomItemInfo[]>(userData["Inventory"].Value);
// Per-user vanity ownership: the player owns the cosmetics they've bought (VanityItems)
// plus whatever is currently equipped (CharacterVanity / GlobalVanity), so the starter
// outfit stays owned. Previously the whole Vanities catalogue was returned as owned,
// which made every item in the Aurum shop show up as already "owned".
var ownedVanityIds = CollectOwnedVanityIds(userData);
var titleData = _titleDataService.Find(new List<string>{"Blueprints"});
// 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)) {
@@ -416,9 +275,9 @@ public class ClientController : Controller
});
}
foreach (var vanityId in ownedVanityIds) {
foreach (var item in vanity) {
items.Add(new FItemInstance {
ItemId = vanityId,
ItemId = item.Name,
// ItemInstanceId is not required since it's not a unique item.
ItemClass = "Vanity",
// PurchaseDate = DateTime.Now, // TODO
@@ -437,66 +296,12 @@ public class ClientController : Controller
Data = new FGetUserInventoryResult
{
Inventory = items,
VirtualCurrency = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value),
VirtualCurrency = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value),
VirtualCurrencyRechargeTimes = new Dictionary<string, FVirtualCurrencyRechargeTime>()
}
});
}
// Collects the vanity ids a player actually owns: everything they've purchased
// (VanityItems) plus everything currently equipped (CharacterVanity slots and the
// GlobalVanity sprays / banner / emotes / drop pod). Parsed defensively with
// JsonDocument so malformed / partial data never breaks the inventory response.
private static HashSet<string> CollectOwnedVanityIds(Dictionary<string, FUserDataRecord> userData)
{
var owned = new HashSet<string>(StringComparer.Ordinal);
void AddId(string? id) { if (!string.IsNullOrEmpty(id)) owned.Add(id); }
if (userData.TryGetValue("VanityItems", out var viRec) && !string.IsNullOrWhiteSpace(viRec.Value))
{
try
{
using var doc = JsonDocument.Parse(viRec.Value);
if (doc.RootElement.ValueKind == JsonValueKind.Array)
foreach (var el in doc.RootElement.EnumerateArray())
if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty("baseItemId", out var b))
AddId(b.GetString());
}
catch { /* ignore malformed VanityItems */ }
}
if (userData.TryGetValue("CharacterVanity", out var cvRec) && !string.IsNullOrWhiteSpace(cvRec.Value))
{
try
{
using var doc = JsonDocument.Parse(cvRec.Value);
if (doc.RootElement.ValueKind == JsonValueKind.Object)
foreach (var prop in doc.RootElement.EnumerateObject())
if (prop.Value.ValueKind == JsonValueKind.Object && prop.Value.TryGetProperty("id", out var id))
AddId(id.GetString());
}
catch { /* ignore malformed CharacterVanity */ }
}
if (userData.TryGetValue("GlobalVanity", out var gvRec) && !string.IsNullOrWhiteSpace(gvRec.Value))
{
try
{
using var doc = JsonDocument.Parse(gvRec.Value);
var root = doc.RootElement;
if (root.ValueKind == JsonValueKind.Object)
{
if (root.TryGetProperty("activeGlobalVanityIds", out var arr) && arr.ValueKind == JsonValueKind.Array)
foreach (var el in arr.EnumerateArray()) AddId(el.GetString());
if (root.TryGetProperty("droppodId", out var dp)) AddId(dp.GetString());
}
}
catch { /* ignore malformed GlobalVanity */ }
}
return owned;
}
[HttpPost("GetTitleData")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
+2 -21
View File
@@ -1,31 +1,12 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR;
namespace Prospect.Server.Api.Hubs;
public class CycleHub : Hub
{
private readonly SignalRConnectionRegistry _registry;
public CycleHub(SignalRConnectionRegistry registry)
{
_registry = registry;
}
public override async Task OnConnectedAsync()
{
// The client connects with the SignalR URL returned by GetSignalRConnection, which
// carries a `uid` query param — use it to map this connection to the player so
// server-initiated messages can target a single player instead of everyone.
var uid = Context.GetHttpContext()?.Request.Query["uid"].ToString();
if (!string.IsNullOrEmpty(uid))
_registry.Add(uid, Context.ConnectionId);
Console.WriteLine("Connected {0} (uid={1})", Context.ConnectionId, uid);
Console.WriteLine("Connected {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
_registry.Remove(Context.ConnectionId);
await base.OnDisconnectedAsync(exception);
}
}
@@ -1,29 +0,0 @@
using System.Collections.Concurrent;
namespace Prospect.Server.Api.Hubs;
// Maps players to their live SignalR connection so server-initiated messages (e.g. the
// matchmaking-success "travel to match" signal) can target a single player instead of being
// broadcast to everyone. The client's SignalR URL carries a `uid` query param (added by
// GetSignalRConnection); CycleHub records it on connect.
public class SignalRConnectionRegistry
{
private readonly ConcurrentDictionary<string, string> _userToConn = new();
private readonly ConcurrentDictionary<string, string> _connToUser = new();
public void Add(string userId, string connectionId)
{
if (string.IsNullOrEmpty(userId)) return;
_userToConn[userId] = connectionId;
_connToUser[connectionId] = userId;
}
public void Remove(string connectionId)
{
if (_connToUser.TryRemove(connectionId, out var userId))
_userToConn.TryRemove(userId, out _);
}
public string? GetConnection(string userId)
=> _userToConn.TryGetValue(userId, out var c) ? c : null;
}
@@ -1,39 +0,0 @@
using System.Text.Json.Serialization;
namespace Prospect.Server.Api.Models.Client.Data;
// PlayFab StoreItem: a single catalog item offered for sale in a store, with its prices.
public class FStoreItem
{
/// <summary>
/// [optional] Store specific custom data. The data only exists as part of this store; it is not transferred to item instances.
/// </summary>
[JsonPropertyName("CustomData")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public object? CustomData { get; set; }
/// <summary>
/// [optional] Intended display position for this item. Note that 0 is the first position and -1 is the last position.
/// </summary>
[JsonPropertyName("DisplayPosition")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public uint? DisplayPosition { get; set; }
/// <summary>
/// Unique identifier of the item as it exists in the catalog - note that this must exactly match the ItemId from the catalog.
/// </summary>
[JsonPropertyName("ItemId")]
public string ItemId { get; set; } = "";
/// <summary>
/// [optional] Override prices for this item for specific currencies.
/// </summary>
[JsonPropertyName("RealCurrencyPrices")]
public Dictionary<string, uint> RealCurrencyPrices { get; set; } = new();
/// <summary>
/// [optional] Override prices for this item in virtual currencies and "RM" (the base Real Money purchase price, in USD pennies).
/// </summary>
[JsonPropertyName("VirtualCurrencyPrices")]
public Dictionary<string, uint> VirtualCurrencyPrices { get; set; } = new();
}
@@ -1,19 +1,8 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
namespace Prospect.Server.Api.Models.Client;
public class FGetStoreItems
{
/// <summary>
/// [optional] Catalog version to store items from. Use default catalog version if null.
/// </summary>
[JsonPropertyName("CatalogVersion")]
public string? CatalogVersion { get; set; }
/// <summary>
/// Unique identifier for the store which is being requested.
/// </summary>
[JsonPropertyName("StoreId")]
public string? StoreId { get; set; }
}
@@ -1,25 +0,0 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
namespace Prospect.Server.Api.Models.Client;
public class FGetStoreItemsResult
{
/// <summary>
/// [optional] The base catalog that this store is a part of.
/// </summary>
[JsonPropertyName("CatalogVersion")]
public string? CatalogVersion { get; set; }
/// <summary>
/// [optional] Array of items which can be purchased from this store.
/// </summary>
[JsonPropertyName("Store")]
public List<FStoreItem> Store { get; set; } = new();
/// <summary>
/// [optional] The ID of this store.
/// </summary>
[JsonPropertyName("StoreId")]
public string? StoreId { get; set; }
}
@@ -1,6 +1,5 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
using Prospect.Server.Api.Models.Data;
namespace Prospect.Server.Api.Models.Client;
@@ -18,7 +17,7 @@ public class FGetUserInventoryResult
/// </summary>
[JsonPropertyName("VirtualCurrency")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public PlayerBalance? VirtualCurrency { get; set; }
public Dictionary<string, int>? VirtualCurrency { get; set; }
/// <summary>
/// [optional] Array of virtual currency balance(s) belonging to the user.
@@ -1,25 +0,0 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
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 InsuranceCurrency { get; set; }
}
public class JobBoardsData
{
[JsonPropertyName("lastBoardRefreshTimeUtc")]
public FYTimestamp LastBoardRefreshTimeUtc { get; set; }
[JsonPropertyName("boards")]
public List<FYFactionContractsData> Boards { get; set; }
}
@@ -15,8 +15,6 @@ public class TitleDataContractInfo {
public TitleDataContractInfoUnlockData UnlockData { get; set; }
[JsonPropertyName("IsMainContract")]
public bool IsMainContract { get; set; }
[JsonPropertyName("ContractDifficulty")]
public EYContractDifficulty ContractDifficulty { get; set; }
}
public class TitleDataContractInfoReward {
@@ -28,127 +26,16 @@ public class TitleDataContractInfoReward {
public class TitleDataContractInfoObjective {
[JsonPropertyName("Type")]
public EYContractObjectiveType Type { get; set; }
public int Type { get; set; }
[JsonPropertyName("MaxProgress")]
public int MaxProgress { get; set; }
[JsonPropertyName("ItemToOwn")]
public string ItemToOwn { get; set; }
[JsonPropertyName("KillConditions")]
public TitleDataContractInfoObjectiveKillConditions? KillConditions { get; set; }
}
public class TitleDataContractInfoObjectiveKillConditions
{
[JsonPropertyName("KillTarget")]
public EYKillTypeAction KillTarget { get; set; }
[JsonPropertyName("AllowedWeaponCategories")]
public EYDeviceCategory[] AllowedWeaponCategories { get; set; }
[JsonPropertyName("AllowedSpecificWeapons")]
public string[] AllowedSpecificWeapons { get; set; }
[JsonPropertyName("SpecificAIEnemyTypeToKill")]
public EYEnemyType SpecificAIEnemyTypeToKill { get; set; }
[JsonPropertyName("MapName")]
public string MapName { get; set; }
[JsonPropertyName("OnlyDuringStorm")]
public bool OnlyDuringStorm { get; set; }
}
public class TitleDataContractInfoUnlockData {
[JsonPropertyName("Level")]
public int Level { get; set; }
[JsonPropertyName("Contracts")]
public HashSet<string> Contracts { get; set; }
public 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 static class ContractObjectiveTypeExtensions {
// Objectives that can only be observed while a raid is running (kills, dead-drops,
// visiting an area, looting a container). The raid is hosted by the player's own
// client and there is no dedicated game server, so their progress is never reported
// to the backend (UpdatePlayerActiveContracts is never called). They are therefore
// auto-credited server-side, otherwise these contracts can never be completed.
// OwnNumOfItem is excluded (validated against the real stash) and so are the meta
// objectives FactionLevel / CompletedMission (validated from persisted player data).
public static bool IsRaidRuntime(this EYContractObjectiveType type) =>
type is EYContractObjectiveType.Kills
or EYContractObjectiveType.DeadDrop
or EYContractObjectiveType.VisitArea
or EYContractObjectiveType.LootContainer;
}
public enum EYContractDifficulty
{
Invalid = 0,
Easy = 1,
Medium = 2,
Hard = 3,
MAX = 4
};
public enum EYDeviceCategory
{
AssaultRifle = 0,
Pistol = 1,
SMG = 2,
SniperRifle = 3,
HeavyWeapon = 4,
Shotgun = 5,
BurstRifle = 6,
Exotic = 7,
MissileLauncher = 8,
Scanner = 9,
All = 10,
INVALID = 11,
MAX = 12
};
public enum EYKillTypeAction
{
Invalid = 0,
Players = 1,
Creatures = 2,
All = 3,
MAX = 4
};
public enum EYEnemyType
{
None = 0,
DebugAutomationTest = 1,
DirtBeast_Melee = 2,
DirtBeast_RangedShort = 3,
DirtBeast_RangedMedium = 4,
DirtBeast_RangedLong = 5,
DirtBeast_MeleeHeavy = 6,
DirtBeast_RangedHeavy = 7,
DirtBeast_FlyingHeavy = 8,
DirtBeast_Boss = 9,
Orobot_Melee = 10,
Orobot_RangedShort = 11,
Orobot_RangedMedium = 12,
Orobot_Walker = 13,
Orobot_Platform = 14,
Plunderbot_RangedShort = 15,
Plunderbot_RangedMedium = 16,
Plunderbot_RangedLong = 17,
GlowBeetle_Blast = 18,
GlowBeetle_Acid = 19,
GlowBeetle_Summon = 20,
Strider = 21,
Rattler = 22,
Weremole = 23,
Crusher = 24,
Howler = 25,
MAX = 26
};
@@ -28,11 +28,11 @@ public class TitleDataBlueprintInfo {
[JsonPropertyName("ScrappingFactionProgressionIncrement")]
public int ScrappingFactionProgressionIncrement { get; set; }
[JsonPropertyName("UnlockData")]
public TitleDataItemUnlockData UnlockData { get; set; }
public object UnlockData { get; set; }
[JsonPropertyName("ModID")]
public int? ModID { get; set; }
[JsonPropertyName("AlienForgeCatalystToUpgradedItemMap")]
public TitleDataItemAlienForgeCatalystToUpgradedItemMap AlienForgeCatalystToUpgradedItemMap { get; set; }
public object? AlienForgeCatalystToUpgradedItemMap { get; set; }
[JsonPropertyName("Rarity")]
public int Rarity { get; set; }
[JsonPropertyName("Kind")]
@@ -61,22 +61,6 @@ public class TitleDataItemRecipeCostType {
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
{
@@ -7,7 +7,7 @@
"ASPNETCORE_Kestrel__Certificates__Default__Path": ".\\certificate.pfx"
},
"dotnetRunMessages": true,
"applicationUrl": "https://127.0.0.1:8443;http://127.0.0.1:8000",
"applicationUrl": "https://2EA46.playfabapi.com:443",
"useSSL": true
},
"IIS Express": {
@@ -7,8 +7,7 @@
"ASPNETCORE_Kestrel__Certificates__Default__Path": ".\\certificate.pfx"
},
"dotnetRunMessages": true,
"applicationUrl": "https://127.0.0.1:8443;http://127.0.0.1:8000",
"useSSL": true
"applicationUrl": "https://2EA46.playfabapi.com"
},
"IIS Express": {
"commandName": "IISExpress",
@@ -4,23 +4,6 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings>
<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>
<ItemGroup>
@@ -55,11 +55,12 @@ public class ActivateContract : ICloudScriptFunction<FYActivateContractRequest,
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", "ContractsOneTimeCompleted"});
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"]);
if (!contracts.TryGetValue(request.ContractID, out var contract)) {
var contract = contracts[request.ContractID];
if (contract == null) {
return new FYActivateContractResult
{
UserID = userId,
@@ -67,32 +68,14 @@ public class ActivateContract : ICloudScriptFunction<FYActivateContractRequest,
Status = 3 // EYActivateContractRequestStatus::FAILED_GETTING_STATIC_DATA
};
}
var contractsCompleted = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(userData["ContractsOneTimeCompleted"].Value);
if (contractsCompleted.ContractsIDs.Contains(request.ContractID)) {
return new FYActivateContractResult
{
UserID = userId,
Error = "Contract is already completed",
Status = 1 // EYActivateContractRequestStatus::CONTRACT_OF_THIS_FACTION_IS_ALREADY_ACTIVE
};
}
// TODO: Contract unlock criteria
var progress = new int[contract.Objectives.Length];
// NOTE: Automatically set max progress for player kill objectives.
for (var i = 0; i < contract.Objectives.Length; i++) {
var objective = contract.Objectives[i];
if (objective.Type == EYContractObjectiveType.Kills && objective.KillConditions.KillTarget == EYKillTypeAction.Players) {
progress[i] = objective.MaxProgress;
}
}
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var contractData = new FYActiveContractPlayerData {
ContractID = request.ContractID,
Progress = progress,
};
// TODO: Max number of active jobs
contractsActive.Contracts.Add(contractData);
await _userDataService.UpdateAsync(
userId, userId,
@@ -32,13 +32,18 @@ public class CheckClientVersionUpToDateFunction : ICloudScriptFunction<CheckClie
_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
{
#if SEASON_2_RELEASE || SEASON_2_DEBUG
IsClientUpToDate = request.Version == "4.27.2-1413204+PROSPECT-Releases-R2.7.0",
#else
IsClientUpToDate = true,
#endif
};
}
}
@@ -1,6 +1,5 @@
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;
@@ -50,8 +49,6 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly string[] difficulties = ["Easy", "Medium", "Hard"];
private readonly Random rnd = new Random();
public ClaimActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
@@ -68,10 +65,11 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
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", "Jobs", "LevelData" });
var titleData = _titleDataService.Find(new List<string>{"Contracts", "Blueprints"});
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
if (!contracts.TryGetValue(request.ContractID, out var contract)) {
var contract = contracts[request.ContractID];
if (contract == null) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
@@ -81,26 +79,18 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
}
var factionKey = "FactionProgression" + contract.Faction;
// Fortuna pass season XP is stored per season; the client derives the pass level
// from this key. No challenge system is emulated, so contracts are the XP source.
#if SEASON_3_RELEASE || SEASON_3_DEBUG
const string seasonXpKey = "FortunaPass3_SeasonXp";
#else
const string seasonXpKey = "FortunaPass2_SeasonXp";
#endif
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey, "JobBoardsData", seasonXpKey }
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 jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var targetContractIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == contract.Name);
var targetContractIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == request.ContractID);
if (targetContractIdx == -1) {
return new FYClaimCompletedActiveContractRewardsResult
{
@@ -112,17 +102,14 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var targetContract = contractsActive.Contracts[targetContractIdx];
List<FYCustomItemInfo> itemsUpdatedOrRemoved = [];
HashSet<string> deletedItemsIds = [];
// TODO: Optimize
for (var i = 0; i < contract.Objectives.Length; i++) {
var objective = contract.Objectives[i];
// TODO: Proper progress verification of all objectives
foreach (var objective in contract.Objectives) {
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 (objective.Type == 2) {
for (var i = 0; i < inventory.Count; i++) {
var item = inventory[i];
if (item.BaseItemId != objective.ItemToOwn) {
continue;
}
@@ -138,18 +125,6 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
break;
}
}
} else if (objective.Type.IsRaidRuntime()) {
// Raid-runtime objectives (kills, dead-drops, visited areas, looted
// containers) can't be tracked in a client-hosted raid — no dedicated
// game server reports them, so their progress is never saved. They are
// auto-credited on deploy (EnterMatchmakingMatch); accept them here too
// (safety net for a contract activated after deploy) so these contracts
// remain completable instead of being stuck on "Objective not met".
remaining = 0;
} else {
// Meta objectives (FactionLevel / CompletedMission): validated from the
// persisted progress. The objectives are mapped by array index.
remaining -= targetContract.Progress[i];
}
if (remaining > 0) {
return new FYClaimCompletedActiveContractRewardsResult
@@ -173,19 +148,17 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
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 });
balance["SC"] += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] });
} else if (reward.ItemID == "Aurum") {
balance.HardCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else if (reward.ItemID == "InsuranceCurrency") {
balance.InsuranceCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
balance["AU"] += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] });
} else if (reward.ItemID == "InsuranceToken") {
balance["IN"] += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceToken", Amount = balance["IN"] });
} else {
// TODO: Inventory limit check
var blueprintData = blueprints[reward.ItemID];
var remainingAmount = reward.Amount; // Quest rewards are given individually
var remainingAmount = reward.Amount * blueprintData.AmountPerPurchase;
// Populate incomplete stacks first
foreach (var item in newInventory) {
@@ -228,47 +201,14 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
factionProgression += contract.ReputationIncrease;
contractsActive.Contracts.RemoveAt(targetContractIdx);
// Main contracts can be completed only once.
// Other contracts should be jobs.
var newContractIdOnBoard = "";
if (contract.IsMainContract) {
contractsCompleted.ContractsIDs.Add(contract.Name);
} else {
var parts = contract.Name.Split('-');
var difficulty = parts[1];
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(titleData["LevelData"]);
var levels = levelsData[contract.Faction];
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"]);
var level = levels.Length - Array.FindIndex(levels, (int xp) => {
return factionProgression >= xp;
});
var factionBoard = jobBoardsData.Boards.Find(c => c.FactionId == contract.Faction);
var idx = Array.FindIndex(difficulties, d => d == difficulty);
var factionJobs = jobs[contract.Faction];
var jobsAvailable = Array.FindAll(factionJobs[difficulty], (string jobId) => {
return level >= contracts[jobId].UnlockData.Level;
});
// If player completes a job, it means he has at least one unlocked anyway.
newContractIdOnBoard = jobsAvailable[rnd.Next(jobsAvailable.Length)];
factionBoard.Contracts[idx].ContractId = newContractIdOnBoard;
contractsCompleted.ContractsIDs.Add(request.ContractID);
}
// TODO: Check for next unlock in contracts instead of relying on client
var newContracts = new List<FYActiveContractPlayerData>();
foreach (var contractIdToUnlock in request.ContractsToUnlock) {
if (contractsCompleted.ContractsIDs.Contains(contractIdToUnlock)) {
continue;
}
var contractToUnlock = contracts[contractIdToUnlock];
var progress = new int[contractToUnlock.Objectives.Length];
// NOTE: Automatically set max progress for player kill objectives.
for (var i = 0; i < contractToUnlock.Objectives.Length; i++) {
var objective = contractToUnlock.Objectives[i];
if (objective.Type == EYContractObjectiveType.Kills && objective.KillConditions.KillTarget == EYKillTypeAction.Players) {
progress[i] = objective.MaxProgress;
}
}
var newContract = new FYActiveContractPlayerData {
ContractID = contractIdToUnlock,
Progress = progress,
@@ -277,12 +217,6 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
newContracts.Add(newContract);
}
// Grant Fortuna pass season XP for completing this contract and persist it, so the
// pass level (derived client-side from FortunaPass{season}_SeasonXp) actually moves.
// No exact rate ships in the data, so scale it to the contract's reputation value.
var seasonXp = int.TryParse(userData[seasonXpKey].Value, out var currentSeasonXp) ? currentSeasonXp : 0;
seasonXp += contract.ReputationIncrease * 10;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
@@ -291,8 +225,6 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(newInventory),
[factionKey] = JsonSerializer.Serialize(factionProgression),
["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData),
[seasonXpKey] = JsonSerializer.Serialize(seasonXp),
}
);
@@ -300,18 +232,18 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
{
UserID = userId,
Error = "",
ClaimedContractID = contract.Name,
ClaimedContractID = request.ContractID,
ContractsActivated = newContracts,
ItemsUpdatedOrRemoved = itemsUpdatedOrRemoved,
ChangedCurrencies = changedCurrencies.ToArray(),
ItemsGranted = itemsGranted,
NewContractIdOnBoard = newContractIdOnBoard,
NewContractIdOnBoard = "", // TODO: This should be a replacement contract apparently?
PlayerFactionProgressData = new FYPlayerFactionProgressData {
FactionID = contract.Faction,
CurrentProgression = factionProgression,
},
Status = 18, // EYClaimContractRewardsStatus::OK
UpdatedSeasonXp = seasonXp // new season XP total (client derives the pass level from it)
UpdatedSeasonXp = 0 // TODO: Probably a separate season XP rate configured by server?
};
}
}
@@ -1,6 +1,5 @@
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;
@@ -104,7 +103,7 @@ public class ClaimGeneratorIncomeClient : ICloudScriptFunction<FYClaimGeneratorI
};
}
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var techTreeBonuses = JsonSerializer.Deserialize<CharacterTechTreeBonuses>(userData["CharacterTechTreeBonuses"].Value);
@@ -137,8 +136,8 @@ public class ClaimGeneratorIncomeClient : ICloudScriptFunction<FYClaimGeneratorI
};
}
balance.SoftCurrency += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
balance["SC"] += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] }];
break;
}
case "playerquarters_gen_aurum": {
@@ -163,8 +162,8 @@ public class ClaimGeneratorIncomeClient : ICloudScriptFunction<FYClaimGeneratorI
};
}
balance.HardCurrency += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
balance["AU"] += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] }];
break;
}
case "playerquarters_gen_crate": {
@@ -191,8 +190,7 @@ public class ClaimGeneratorIncomeClient : ICloudScriptFunction<FYClaimGeneratorI
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);
var items = PickItems(rewardsPool, 5); // TODO: Not sure how many items were granted
// TODO: Stackable? GrantedItems doesn't imply granted OR updated.
foreach (var item in items) {
var itemInfo = blueprints[item.Name];
@@ -1,8 +1,5 @@
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 ClientsideFriendsImportRequest
{
@@ -21,33 +18,15 @@ public class ClientsideFriendsImportResponse
public class ClientsideFriendsImportFunction : ICloudScriptFunction<ClientsideFriendsImportRequest, ClientsideFriendsImportResponse>
{
private readonly ILogger<ClientsideFriendsImportFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public ClientsideFriendsImportFunction(ILogger<ClientsideFriendsImportFunction> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
public ClientsideFriendsImportFunction(ILogger<ClientsideFriendsImportFunction> logger)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<ClientsideFriendsImportResponse> ExecuteAsync(ClientsideFriendsImportRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// Persist the platform friend ids the client imports (Steam ids) so GetFriendList
// can later resolve which of them actually play on this server.
var ids = request.UserIDs ?? Array.Empty<string>();
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>
{
["ImportedSteamFriends"] = JsonSerializer.Serialize(ids),
});
_logger.LogInformation("Imported {Count} {Platform} friend id(s) for user {User}", ids.Length, request.Platform, userId);
_logger.LogInformation("Processing friends import");
return new ClientsideFriendsImportResponse{};
}
@@ -1,154 +0,0 @@
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.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
// Closed Beta Function
public class CompleteInventoryUpdateRequest
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("reason")]
public string Reason { get; set; }
[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 CompleteInventoryUpdateResponse
{
[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 FYCustomItemInfo[] ItemsToUpdateAmount { get; set; }
[JsonPropertyName("itemsToRemove")]
public HashSet<string> ItemsToRemove { get; set; }
}
[CloudScriptFunction("CompleteInventoryUpdate")]
public class CompleteInventoryUpdateFunction : ICloudScriptFunction<CompleteInventoryUpdateRequest, CompleteInventoryUpdateResponse>
{
private readonly ILogger<CompleteInventoryUpdateFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public CompleteInventoryUpdateFunction(ILogger<CompleteInventoryUpdateFunction> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<CompleteInventoryUpdateResponse> ExecuteAsync(CompleteInventoryUpdateRequest 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);
// Insurance payout: this request also reports the end-of-match inventory, so items
// the player lost (death / left behind) arrive in ItemsToRemove. Any of those that
// were insured on deploy (EnterMatchmakingStation) are recovered back to the stash —
// the premium was already paid up-front — and the insurance is consumed (one raid).
// Recovery is only performed if the item's policy declares a positive Payout.
var recoveredInsured = new HashSet<string>();
if (request.ItemsToRemove is { Count: > 0 })
{
try
{
var td = _titleDataService.Find(new List<string> { "InventoryInsurance" });
if (td.TryGetValue("InventoryInsurance", out var rawPolicies) && !string.IsNullOrEmpty(rawPolicies))
{
var policies = JsonSerializer.Deserialize<Dictionary<string, InventoryInsurancePolicy>>(rawPolicies);
foreach (var item in inventory)
{
if (!request.ItemsToRemove.Contains(item.ItemId)) continue;
if (string.IsNullOrEmpty(item.Insurance) || item.InsuranceOwnerPlayfabId != userId) continue;
if (policies == null || !policies.TryGetValue(item.Insurance, out var policy) || policy.Payout <= 0) continue;
recoveredInsured.Add(item.ItemId);
item.Insurance = ""; // consumed for this raid
item.InsuranceOwnerPlayfabId = "";
}
if (recoveredInsured.Count > 0)
{
_logger.LogInformation("Recovered {Count} insured item(s) for user {User}", recoveredInsured.Count, userId);
}
}
}
catch (Exception e)
{
// Never fail the inventory commit because of insurance handling.
_logger.LogWarning(e, "Failed to process insurance payout");
}
}
// 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.
// IMPORTANT: The same request is currently used to report the updated inventory in the end of match.
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
// Keep items that weren't removed, plus insured items recovered above.
if (!request.ItemsToRemove.Contains(item.ItemId) || recoveredInsured.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;
inventoryItem.Durability = item.Durability;
}
// 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 CompleteInventoryUpdateResponse
{
UserId = userId,
//Error = "",
//NewSet = request.NewSet,
//ItemsToAdd = request.ItemsToAdd,
//ItemsToRemove = request.ItemsToRemove,
//ItemsToUpdateAmount = request.ItemsToUpdateAmount,
};
}
}
@@ -1,56 +0,0 @@
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SignalR;
using Prospect.Server.Api.Hubs;
using Prospect.Server.Api.Services.CloudScript.Models;
// Closed Beta Function
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("EnterMatchmaking")]
public class EnterMatchmakingFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchAzureFunctionResult>
{
private readonly IHubContext<CycleHub> _hubContext;
public EnterMatchmakingFunction(IHubContext<CycleHub> hubContext)
{
_hubContext = hubContext;
}
public async Task<FYEnterMatchAzureFunctionResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
//await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
// Success = true,
// SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
// SquadID = request.SquadId,
//});
// Dedicated-server mode (2nd preprod): if GAMESERVER_ADDRESS ("host:port") is set, hand
// the client the real game server to travel to instead of the client-hosted station.
// Same image as preprod — only this env var flips the deployment type.
var gameServer = Environment.GetEnvironmentVariable("GAMESERVER_ADDRESS");
if (!string.IsNullOrEmpty(gameServer))
{
var parts = gameServer.Split(':');
return new FYEnterMatchAzureFunctionResult
{
Success = true,
ErrorMessage = "",
SingleplayerStation = false,
Address = parts[0],
Port = parts.Length > 1 && int.TryParse(parts[1], out var gsPort) ? gsPort : 7777,
MaintenanceMode = false,
};
}
return new FYEnterMatchAzureFunctionResult
{
Success = true,
ErrorMessage = "",
SingleplayerStation = true,
Address = request.MapName,
MaintenanceMode = false,
Port = 7777,
};
}
}
@@ -1,10 +1,7 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SignalR;
using Prospect.Server.Api.Hubs;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
@@ -20,104 +17,20 @@ public class OnSquadMatchmakingSuccessMessage {
[CloudScriptFunction("EnterMatchmakingMatch")]
public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHubContext<CycleHub> _hubContext;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly SignalRConnectionRegistry _registry;
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, SignalRConnectionRegistry registry)
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext)
{
_httpContextAccessor = httpContextAccessor;
_hubContext = hubContext;
_userDataService = userDataService;
_titleDataService = titleDataService;
_registry = registry;
}
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "Inventory"}
);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
// Compute delivery quest progress before deploy.
// This ensures that the quest progress will be shown on the planet.
// The station doesn't seem to care about delivery quests progress and calculates progress
// based on actual items in stash. And so does the "ClaimActiveContract" function.
foreach (var contractActive in contractsActive.Contracts) {
if (!contracts.TryGetValue(contractActive.ContractID, out var contract)) {
continue;
}
for (var i = 0; i < contract.Objectives.Length; i++) {
if (i >= contractActive.Progress.Length) {
// Defensive: player progress array shorter than the objective list.
break;
}
var objective = contract.Objectives[i];
// Raid-runtime objectives (kills, dead-drops, visited areas, looted
// containers) are simulated inside the client-hosted raid; with no
// dedicated game server their progress is never reported to the backend,
// so they would never be saved. Auto-credit them on deploy so the
// objective persists and the station shows the contract as completable.
if (objective.Type.IsRaidRuntime()) {
contractActive.Progress[i] = objective.MaxProgress;
continue;
}
if (objective.Type != EYContractObjectiveType.OwnNumOfItem) {
continue;
}
int remaining = objective.MaxProgress;
foreach (var item in inventory) {
if (item.BaseItemId != objective.ItemToOwn) {
continue;
}
remaining -= item.Amount;
if (remaining <= 0) {
remaining = 0;
break;
}
}
contractActive.Progress[i] = objective.MaxProgress - remaining;
}
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["ContractsActive"] = JsonSerializer.Serialize(contractsActive),
}
);
// Send the "matchmaking succeeded / travel to match" signal only to the deploying
// player (via their mapped SignalR connection) — broadcasting to Clients.All made
// every connected player travel too. Fall back to broadcast only if the connection
// isn't mapped yet.
// Dedicated-server mode (2nd preprod): point the client at the real game server address
// to travel to; otherwise keep the map name (client-hosted). Same image — env flips it.
var gameServer = Environment.GetEnvironmentVariable("GAMESERVER_ADDRESS");
var deployMessage = new OnSquadMatchmakingSuccessMessage {
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
Success = true,
SessionID = string.IsNullOrEmpty(gameServer) ? request.MapName : gameServer,
SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
SquadID = request.SquadId,
};
var connectionId = _registry.GetConnection(userId);
if (connectionId != null)
await _hubContext.Clients.Client(connectionId).SendAsync("OnSquadMatchmakingSuccess", deployMessage);
else
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", deployMessage);
});
return new FYEnterMatchmakingResult
{
@@ -1,107 +1,18 @@
using System.Text.Json;
using Prospect.Server.Api.Models.Data;
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;
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
// Config d'une police d'assurance (TitleData "InventoryInsurance").
public class InventoryInsurancePolicy
{
public string InsuranceId { get; set; }
public double Cost { get; set; }
public double Payout { get; set; }
}
[CloudScriptFunction("EnterMatchmakingStation")]
public class EnterMatchmakingStationFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<EnterMatchmakingStationFunction> _logger;
public EnterMatchmakingStationFunction(
IHttpContextAccessor httpContextAccessor,
UserDataService userDataService,
TitleDataService titleDataService,
ILogger<EnterMatchmakingStationFunction> logger)
public Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
// Au déploiement, le client peut demander d'assurer une partie de l'inventaire.
var insurance = request.PurchaseInventoryInsurances;
var context = _httpContextAccessor.HttpContext;
if (context != null && insurance?.ItemIds is { Count: > 0 })
{
try
{
await ApplyInsuranceAsync(context.User.FindAuthUserId(), insurance);
}
catch (Exception e)
{
// Ne jamais bloquer le déploiement à cause de l'assurance.
_logger.LogWarning(e, "Failed to apply inventory insurance on deploy");
}
}
return new FYEnterMatchmakingResult
return Task.FromResult(new FYEnterMatchmakingResult
{
Success = true,
ErrorMessage = "",
SingleplayerStation = true, // NOTE: This will always travel to single player station
IsMatchTravel = false,
};
}
// Marque les items choisis comme assurés (Insurance + propriétaire) et débite la
// prime en InsuranceCurrency selon le coût de la police (TitleData InventoryInsurance).
private async Task ApplyInsuranceAsync(string userId, FYPurchaseInventoryInsuranceRequest req)
{
var titleData = _titleDataService.Find(new List<string> { "InventoryInsurance" });
var policies = JsonSerializer.Deserialize<Dictionary<string, InventoryInsurancePolicy>>(titleData["InventoryInsurance"]);
var insuranceId = string.IsNullOrEmpty(req.InsuranceId) ? "Default" : req.InsuranceId;
if (policies == null || !policies.TryGetValue(insuranceId, out var policy))
{
_logger.LogWarning("Unknown insurance policy {InsuranceId}", insuranceId);
return;
}
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 toInsure = new HashSet<string>(req.ItemIds);
var insuredCount = 0;
foreach (var item in inventory)
{
if (!toInsure.Contains(item.ItemId)) continue;
item.Insurance = insuranceId;
item.InsuranceOwnerPlayfabId = userId;
insuredCount++;
}
if (insuredCount == 0) return;
// Prime = coût de la police × nombre d'items assurés (arrondi au supérieur),
// plafonnée au solde disponible pour ne jamais passer en négatif.
var premium = (int)Math.Ceiling(policy.Cost * insuredCount);
premium = Math.Min(premium, balance.InsuranceCurrency);
balance.InsuranceCurrency -= premium;
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>
{
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
});
_logger.LogInformation("Insured {Count} item(s) under policy {Policy} for user {User} (premium {Premium})",
insuredCount, insuranceId, userId, premium);
}
}
@@ -1,8 +1,6 @@
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
@@ -10,15 +8,13 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class GetCraftingInProgressData : ICloudScriptFunction<FYGetCraftingInProgressDataRequest, FYGetCraftingInProgressDataResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetCraftingInProgressData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
public GetCraftingInProgressData(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYGetCraftingInProgressDataResult> ExecuteAsync(FYGetCraftingInProgressDataRequest request)
public Task<FYGetCraftingInProgressDataResult> ExecuteAsync(FYGetCraftingInProgressDataRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
@@ -26,20 +22,11 @@ public class GetCraftingInProgressData : ICloudScriptFunction<FYGetCraftingInPro
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// Return the persisted in-progress craft (written by StartItemCraftingClient)
// instead of an empty stub, so the crafting timer survives a menu reopen / relog.
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "CraftingTimer__2022_05_12" });
// Fully qualified: a different FYItemCurrentlyBeingCrafted also exists in this namespace.
var craft = JsonSerializer.Deserialize<Models.Data.FYItemCurrentlyBeingCrafted>(userData["CraftingTimer__2022_05_12"].Value)
?? new Models.Data.FYItemCurrentlyBeingCrafted();
return new FYGetCraftingInProgressDataResult
return Task.FromResult(new FYGetCraftingInProgressDataResult
{
UserId = userId,
UserId = context.User.FindAuthUserId(),
Error = string.Empty,
ItemCurrentlyBeingCrafted = craft,
};
ItemCurrentlyBeingCrafted = {},
});
}
}
@@ -1,90 +1,15 @@
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.Database;
using Prospect.Server.Api.Services.Database.Models;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("GetFriendList")]
public class GetFriendList : ICloudScriptFunction<FYBaseSocialRequest, object?>
{
private readonly ILogger<GetFriendList> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly DbUserService _userService;
public Task<object?> ExecuteAsync(FYBaseSocialRequest request)
{
return Task.FromResult<object?>(new
{
public GetFriendList(ILogger<GetFriendList> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, DbUserService userService)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_userService = userService;
}
public async Task<object?> ExecuteAsync(FYBaseSocialRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// Resolve the imported Steam friends (persisted by ClientsideFriendsImport) to the
// players that actually exist on this server. It's a private server, so only friends
// who have logged in here will show up.
var steamIds = new List<string>();
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "ImportedSteamFriends" });
if (userData.TryGetValue("ImportedSteamFriends", out var rec) && !string.IsNullOrWhiteSpace(rec.Value))
{
try { steamIds = JsonSerializer.Deserialize<List<string>>(rec.Value) ?? new List<string>(); }
catch { /* ignore malformed */ }
}
var friends = new List<object>();
if (steamIds.Count > 0)
{
var players = await _userService.FindManyAsync(PlayFabUserAuthType.Steam, steamIds);
var nowUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
foreach (var player in players)
{
if (player.Id == userId) continue; // never list yourself
// Presence persisted by UpdatePlayerPresenceState: online if seen in the last
// 3 minutes. EYUserState best-effort: 0 = offline, 1 = online, 2 = in match.
var onlineState = 0;
var presenceData = await _userDataService.FindAsync(player.Id, player.Id, new List<string> { "PresenceState" });
if (presenceData.TryGetValue("PresenceState", out var presenceRec) && !string.IsNullOrWhiteSpace(presenceRec.Value))
{
try
{
var presence = JsonSerializer.Deserialize<PlayerPresenceState>(presenceRec.Value);
if (presence != null && nowUtc - presence.LastSeenUtc <= 180)
{
onlineState = presence.InMatch ? 2 : 1;
}
}
catch { /* ignore malformed presence */ }
}
friends.Add(new
{
profile = new
{
playerId = player.Id,
displayName = player.DisplayName,
avatarUrl = "",
},
onlineState,
});
}
}
// NOTE: the exact response shape the client expects is not yet confirmed; this is a
// best-effort structure. The log lets us verify resolution while we validate in game.
_logger.LogInformation("GetFriendList for {User}: {Count} friend(s) resolved on server", userId, friends.Count);
return new { friends };
}
}
@@ -1,7 +1,5 @@
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
@@ -9,31 +7,21 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class GetPlayerSets : ICloudScriptFunction<FYGetPlayersSets, object?>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetPlayerSets(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
public GetPlayerSets(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<object?> ExecuteAsync(FYGetPlayersSets request)
public Task<object?> ExecuteAsync(FYGetPlayersSets request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return null;
return Task.FromResult<object?>(null);
}
var userId = context.User.FindAuthUserId();
// Return the player's persisted loadout (written by CompleteInventoryUpdate /
// RequestUpdateStationInventory) instead of a blank set, so saved gear survives
// a menu reopen / relog. Same response shape as before, just populated.
var userData = await _userDataService.FindAsync(userId, userId, new List<string> { "LOADOUT" });
var loadout = JsonSerializer.Deserialize<LoadoutData>(userData["LOADOUT"].Value) ?? new LoadoutData();
return new
return Task.FromResult<object?>(new
{
success = true,
entries = new []
@@ -43,19 +31,19 @@ public class GetPlayerSets : ICloudScriptFunction<FYGetPlayersSets, object?>
setData = new
{
id = "",
userId,
userId = context.User.FindAuthUserId(),
kit = "",
shield = loadout.Shield ?? "",
helmet = loadout.Helmet ?? "",
weaponOne = loadout.WeaponOne ?? "",
weaponTwo = loadout.WeaponTwo ?? "",
bag = loadout.Bag ?? "",
bagItemsAsJsonStr = loadout.BagItemsAsJsonStr ?? "",
safeItemsAsJsonStr = loadout.SafeItemsAsJsonStr ?? ""
shield = "",
helmet = "",
weaponOne = "",
weaponTwo = "",
bag = "",
bagItemsAsJsonStr = "",
safeItemsAsJsonStr = ""
},
items = Array.Empty<object>()
}
}
};
});
}
}
@@ -8,8 +8,13 @@ public class GetPlayerStatisticsRequestsClientRequest
public string[] Statistics { get; set; }
}
public class GetPlayerStatisticsRequestsClientResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("GetPlayerStatisticsRequestsClient")]
public class GetPlayerStatisticsRequestsClientFunction : ICloudScriptFunction<GetPlayerStatisticsRequestsClientRequest, object>
public class GetPlayerStatisticsRequestsClientFunction : ICloudScriptFunction<GetPlayerStatisticsRequestsClientRequest, GetPlayerStatisticsRequestsClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
@@ -18,7 +23,7 @@ public class GetPlayerStatisticsRequestsClientFunction : ICloudScriptFunction<Ge
_httpContextAccessor = httpContextAccessor;
}
public Task<object> ExecuteAsync(GetPlayerStatisticsRequestsClientRequest request)
public async Task<GetPlayerStatisticsRequestsClientResponse> ExecuteAsync(GetPlayerStatisticsRequestsClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
@@ -26,15 +31,8 @@ public class GetPlayerStatisticsRequestsClientFunction : ICloudScriptFunction<Ge
throw new CloudScriptException("CloudScript was not called within a http request");
}
// Return each requested statistic with a value so the career screen renders instead
// of staying blank. Raid stats (kills/deaths/evacs/damage/…) come from the
// client-hosted raid and aren't tracked server-side, so they report 0 for now.
var stats = new List<object>();
foreach (var name in request.Statistics ?? Array.Empty<string>())
return new GetPlayerStatisticsRequestsClientResponse
{
stats.Add(new { statisticName = name, value = 0 });
}
return Task.FromResult<object>(new { statistics = stats });
};
}
}
@@ -1,72 +1,16 @@
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Options;
using Prospect.Server.Api.Config;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("GetSignalRConnection")]
public class GetSignalRConnection : ICloudScriptFunction<FYGetSignalRConnection, FYGetSignalRConnectionResult>
{
// LAN address of the homelab host. Clients on the local network get the SignalR URL on
// this IP (which their cert already validates via the IP/DNS SAN), while external clients
// keep the public domain from configuration. This matters because the real-time WebSocket
// (libwebsockets, incl. under Proton/Wine) can't reach the public domain from inside the
// LAN (hairpin NAT), which surfaced as "error code 5" right after the intro.
private const string LanHost = "192.168.1.136";
private readonly PlayFabSettings _settings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<GetSignalRConnection> _logger;
public GetSignalRConnection(IOptions<PlayFabSettings> settings, IHttpContextAccessor httpContextAccessor, ILogger<GetSignalRConnection> logger)
{
_settings = settings.Value;
_httpContextAccessor = httpContextAccessor;
_logger = logger;
}
public Task<FYGetSignalRConnectionResult> ExecuteAsync(FYGetSignalRConnection request)
{
// The game client connects to SignalR only over HTTPS.
var url = _settings.SignalRURL;
var remote = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress;
var lan = remote != null && IsLan(remote);
if (lan)
{
try
{
url = new UriBuilder(url) { Host = LanHost }.Uri.AbsoluteUri;
}
catch { /* keep the configured URL on any parsing issue */ }
}
// Tag the connection with the player id so the hub can map connection -> user and
// target server messages (deploy signal) instead of broadcasting to everyone.
var userId = _httpContextAccessor.HttpContext?.User.FindAuthUserId();
if (!string.IsNullOrEmpty(userId))
url += (url.Contains('?') ? "&" : "?") + "uid=" + Uri.EscapeDataString(userId);
_logger.LogInformation("GetSignalRConnection: remoteIp={Remote} lan={Lan} uid={Uid} -> {Url}", remote, lan, userId, url);
return Task.FromResult(new FYGetSignalRConnectionResult
{
Url = url,
AccessToken = _settings.SignalRAccessToken
Url = "https://2ea46.playfabapi.com/signalr/?hub=pubsub",
AccessToken = "TEST"
});
}
private static bool IsLan(IPAddress ip)
{
if (IPAddress.IsLoopback(ip)) return true;
if (ip.IsIPv4MappedToIPv6) ip = ip.MapToIPv4();
if (ip.AddressFamily != AddressFamily.InterNetwork) return false;
var b = ip.GetAddressBytes();
return b[0] == 10
|| (b[0] == 172 && b[1] >= 16 && b[1] <= 31)
|| (b[0] == 192 && b[1] == 168);
}
}
@@ -1,9 +1,7 @@
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.CloudScript.Functions;
using Prospect.Server.Api.Services.UserData;
public class PurchaseWeaponShopItemRequest
@@ -138,9 +136,9 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeapo
Error = "Invalid purchase amount",
};
}
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance", "ContractsOneTimeCompleted"});
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 balance = JsonSerializer.Deserialize<Dictionary<string, int>>(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)) {
@@ -154,23 +152,7 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeapo
var blueprintData = blueprints[request.BaseItemID];
// Contract unlock criteria: some items can only be purchased after completing a
// specific contract. The data uses the sentinel "None" (not an empty string) for
// items with no lock — treat that (and null/empty) as "no lock", otherwise EVERY
// item gets gated and nothing can be bought.
var contractLockPurchase = blueprintData.UnlockData?.ContractLockPurchase;
if (!string.IsNullOrEmpty(contractLockPurchase) && contractLockPurchase != "None")
{
var completed = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(userData["ContractsOneTimeCompleted"].Value);
if (completed?.ContractsIDs == null || !completed.ContractsIDs.Contains(contractLockPurchase))
{
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Item is locked: complete the required contract first",
};
}
}
// TODO: Contract unlock criteria
// Validate price
var shopCraftingData = blueprintData.ItemShopsCraftingData[request.ShopItemBelongsTo];
@@ -183,15 +165,16 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeapo
if (ingredient.Currency == "SoftCurrency") {
var buyCost = request.PurchaseAmount * ingredient.Amount;
remaining -= buyCost;
balance.SoftCurrency -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
balance["SC"] -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] });
} 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 });
balance["AU"] -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] });
} else {
foreach (var item in inventory) {
for (var i = 0; i < inventory.Count; i++) {
var item = inventory[i];
if (item.BaseItemId != ingredient.Currency) {
continue;
}
@@ -226,7 +209,6 @@ public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeapo
}
}
// TODO: Inventory limit check
// TODO: Refactor into a helper function
var remainingAmount = request.PurchaseAmount * blueprintData.AmountPerPurchase;
@@ -1,6 +1,5 @@
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;
@@ -66,20 +65,20 @@ public class RepairItem : ICloudScriptFunction<FYRepairItemRequest, FYRepairItem
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var blueprintData = blueprints[item.BaseItemId];
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var repairCost = blueprintData.RepairCostBase + (int)Math.Ceiling((1 - (float)item.Durability / blueprintData.DurabilityMax) * blueprintData.RepairCostMaxDurability);
var repairCost = MapValue.Map(item.Durability, blueprintData.DurabilityMax, 0, blueprintData.RepairCostBase, blueprintData.RepairCostMaxDurability);
if (item.Durability == 0) {
repairCost += blueprintData.RepairCostModifierBroken;
}
if (balance.SoftCurrency < repairCost) {
if (balance["SC"] < repairCost) {
return new FYRepairItemResult
{
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= repairCost;
balance["SC"] -= repairCost;
item.Durability = blueprintData.DurabilityMax;
await _userDataService.UpdateAsync(
@@ -96,7 +95,7 @@ public class RepairItem : ICloudScriptFunction<FYRepairItemRequest, FYRepairItem
Error = "",
ChangedItems = [item],
ChangedCurrencies = [
new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency },
new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] },
],
};
}
@@ -1,7 +1,4 @@
using System.Collections;
using System.Diagnostics.Contracts;
using System.Text.Json;
using Prospect.Server.Api.Models.Data;
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;
@@ -14,16 +11,11 @@ public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQuery
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly string[] factions = ["Korolev", "Osiris", "ICA"]; // TODO: Order is important
private readonly string[] difficulties = ["Easy", "Medium", "Hard"];
private readonly Random rnd = new Random();
public RequestActiveObjectivesAndBoardsData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
public RequestActiveObjectivesAndBoardsData(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYQueryFactionProgressionRequest request)
@@ -34,65 +26,10 @@ public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQuery
return new FYGetPlayerContractsResult{};
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{ "ContractsActive", "JobBoardsData", "FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris" });
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"ContractsActive"});
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var lastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc;
var boards = jobBoardsData.Boards;
// The boards are updated once in 3 hours
if (now >= lastBoardRefreshTimeUtc.Seconds + 60 * 60 * 3) {
var titleData = _titleDataService.Find(new List<string> { "Contracts", "Jobs", "LevelData" });
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"]);
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(titleData["LevelData"]);
var currentQuests = new Dictionary<string, Dictionary<string, bool>> {
["Korolev"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
["Osiris"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
["ICA"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
};
foreach (var contract in contractsActive.Contracts) {
var parts = contract.ContractID.Split('-');
if (parts[0] != "NEW") {
continue;
}
var difficulty = parts[1];
var faction = parts[2] == "KOR" ? "Korolev" : parts[2];
currentQuests[faction][difficulty] = true;
}
for (var i = 0; i < factions.Length; i++) {
var faction = factions[i];
var factionProgression = JsonSerializer.Deserialize<int>(userData[$"FactionProgression{faction}"].Value);
var levels = levelsData[faction];
var level = levels.Length - Array.FindIndex(levels, (int xp) => {
return factionProgression >= xp;
});
for (var j = 0; j < difficulties.Length; j++) {
var difficulty = difficulties[j];
if (currentQuests[faction][difficulty]) {
continue;
}
var factionJobs = jobs[faction];
var jobsAvailable = Array.FindAll(factionJobs[difficulty], (string jobId) => {
return level >= contracts[jobId].UnlockData.Level;
});
string jobId = jobsAvailable.Length > 0 ? jobsAvailable[rnd.Next(jobsAvailable.Length)] : factionJobs[difficulty][0];
boards[i].Contracts[j] = new FYFactionContractData { ContractId = jobId };
}
}
lastBoardRefreshTimeUtc.Seconds = now;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string> { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) }
);
}
// TODO: Get player's contract boards and refresh
return new FYGetPlayerContractsResult
{
@@ -101,10 +38,72 @@ public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQuery
ActiveContracts = contractsActive.Contracts,
FactionsContracts = new FYFactionsContractsData
{
Boards = boards,
LastBoardRefreshTimeUtc = lastBoardRefreshTimeUtc
Boards = new List<FYFactionContractsData>
{
new FYFactionContractsData
{
FactionId = "ICA",
Contracts = new List<FYFactionContractData>
{
new FYFactionContractData
{
ContractId = "NEW-Easy-ICA-Gather-1",
},
RefreshHours24UtcFromBackend = 12 // Doesn't seem to work
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
};
}
}
@@ -1,6 +1,5 @@
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;
@@ -10,15 +9,6 @@ public class FYFortunaPassClaimedRewards {
public List<string> RewardsIDs { get; set; }
};
// Récompense d'un palier de la passe (catalogue optionnel TitleData "FortunaPass2_Rewards").
// itemId "SoftCurrency"/"Aurum"/"InsuranceCurrency" = monnaie, sinon item d'inventaire.
public class FortunaPassRewardDef {
[JsonPropertyName("itemId")]
public string ItemId { get; set; }
[JsonPropertyName("amount")]
public int Amount { get; set; }
}
public class RequestClaimFortunaPassRewardsRequest
{
[JsonPropertyName("rewardsIds")]
@@ -44,15 +34,11 @@ public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction<Reque
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<RequestClaimFortunaPassRewardsFunction> _logger;
public RequestClaimFortunaPassRewardsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger<RequestClaimFortunaPassRewardsFunction> logger)
public RequestClaimFortunaPassRewardsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<RequestClaimFortunaPassRewardsResponse> ExecuteAsync(RequestClaimFortunaPassRewardsRequest request)
@@ -63,103 +49,28 @@ public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction<Reque
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// Clé de la saison courante (déterminée par la config de build) — évite d'écraser
// la progression de l'autre saison (l'ancien code écrasait S2 ET S3).
#if SEASON_3_RELEASE || SEASON_3_DEBUG
const string claimedKey = "FortunaPass3_ClaimedRewards";
const string rewardsCatalogKey = "FortunaPass3_Rewards";
#else
const string claimedKey = "FortunaPass2_ClaimedRewards";
const string rewardsCatalogKey = "FortunaPass2_Rewards";
#endif
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{ claimedKey, "Inventory", "Balance" });
var claimedRewards = JsonSerializer.Deserialize<FYFortunaPassClaimedRewards>(userData[claimedKey].Value)
?? new FYFortunaPassClaimedRewards { RewardsIDs = new List<string>() };
claimedRewards.RewardsIDs ??= new List<string>();
// Catalogue des récompenses (optionnel) : rewardId -> {itemId, amount}.
Dictionary<string, FortunaPassRewardDef> catalog = null;
try
{
var td = _titleDataService.Find(new List<string> { rewardsCatalogKey });
if (td.TryGetValue(rewardsCatalogKey, out var raw) && !string.IsNullOrWhiteSpace(raw))
catalog = JsonSerializer.Deserialize<Dictionary<string, FortunaPassRewardDef>>(raw);
}
catch { /* pas de catalogue → on enregistre seulement la réclamation */ }
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value) ?? new List<FYCustomItemInfo>();
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var grantedItems = new List<FYCustomItemInfo>();
var changedCurrencies = new List<FYCurrencyItem>();
foreach (var rewardId in request.RewardsIDs)
{
// Ne pas re-créditer un palier déjà réclamé.
if (claimedRewards.RewardsIDs.Contains(rewardId)) continue;
if (catalog == null || !catalog.TryGetValue(rewardId, out var def) || def == null)
{
// Le catalogue des paliers est défini côté client (DataTable). On journalise
// les IDs inconnus pour pouvoir construire un vrai catalogue FortunaPass{2,3}_Rewards
// qui matche. IMPORTANT : ne PAS marquer le palier comme réclamé si on ne peut rien
// accorder — sinon il serait brûlé définitivement et inclaimable une fois le
// catalogue ajouté.
_logger.LogInformation("FortunaPass claim: aucun catalogue pour le reward '{RewardId}' (catalogue {State}) — non accordé", rewardId, catalog == null ? "absent" : "présent");
continue;
}
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);
_logger.LogInformation("FortunaPass claim: octroi reward '{RewardId}' -> {ItemId} x{Amount}", rewardId, def.ItemId, def.Amount);
switch (def.ItemId)
{
case "SoftCurrency":
balance.SoftCurrency += def.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
break;
case "Aurum":
balance.HardCurrency += def.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
break;
case "InsuranceCurrency":
balance.InsuranceCurrency += def.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
break;
default:
var item = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = def.Amount <= 0 ? 1 : def.Amount,
BaseItemId = def.ItemId,
Durability = 0,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(item);
grantedItems.Add(item);
break;
}
}
var changes = new Dictionary<string, string> { [claimedKey] = JsonSerializer.Serialize(claimedRewards) };
if (grantedItems.Count > 0) changes["Inventory"] = JsonSerializer.Serialize(inventory);
if (changedCurrencies.Count > 0) changes["Balance"] = JsonSerializer.Serialize(balance);
await _userDataService.UpdateAsync(userId, userId, changes);
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 = grantedItems.ToArray(),
ChangedCurrencies = changedCurrencies.ToArray(),
GrantedItems = [],
ChangedCurrencies = [],
};
}
}
@@ -1,7 +1,5 @@
using System.Text.Json;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
@@ -9,43 +7,42 @@ namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class RequestFactionProgression : ICloudScriptFunction<FYQueryFactionProgressionRequest, object?>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestFactionProgression(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
public RequestFactionProgression(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<object?> ExecuteAsync(FYQueryFactionProgressionRequest request)
public Task<object?> ExecuteAsync(FYQueryFactionProgressionRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return null;
return Task.FromResult<object?>(null);
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string> { "FactionProgressionICA", "FactionProgressionKorolev", "FactionProgressionOsiris" }
);
int Progression(string faction) =>
userData.TryGetValue("FactionProgression" + faction, out var v) && v.Value != null
? JsonSerializer.Deserialize<int>(v.Value)
: 0;
return new
return Task.FromResult<object?>(new
{
factions = new []
{
new { factionId = "ICA", currentProgression = Progression("ICA") },
new { factionId = "Korolev", currentProgression = Progression("Korolev") },
new { factionId = "Osiris", currentProgression = Progression("Osiris") },
new
{
factionId = "ICA",
currentProgression = 0
},
userId,
new
{
factionId = "Korolev",
currentProgression = 0
},
new
{
factionId = "Osiris",
currentProgression = 0
},
},
userId = context.User.FindAuthUserId(),
error = ""
};
});
}
}
@@ -1,6 +1,5 @@
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;
@@ -55,7 +54,7 @@ public class RequestMissionCompleted : ICloudScriptFunction<FYRequestMissionComp
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 balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var currentMission = missions[onboardingMission.CurrentMissionID];
var missionRewardId = currentMission.OnboardingRewards.RowName;
@@ -69,16 +68,15 @@ public class RequestMissionCompleted : ICloudScriptFunction<FYRequestMissionComp
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 });
balance["SC"] += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] });
} else if (reward.RewardRowHandle.RowName == "Aurum") {
balance.HardCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else if (reward.RewardRowHandle.RowName == "InsuranceCurrency") {
balance.InsuranceCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
balance["AU"] += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] });
} else if (reward.RewardRowHandle.RowName == "InsuranceToken") {
balance["IN"] += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceToken", Amount = balance["IN"] });
} else {
// TODO: Inventory limit check
var blueprintData = blueprints[reward.RewardRowHandle.RowName];
var remainingAmount = reward.RewardAmount * blueprintData.AmountPerPurchase;
while (remainingAmount > 0) {
@@ -1,119 +1,100 @@
using System.Text.Json;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("RequestPlayerContracts")]
public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsRequest, FYGetPlayerContractsResult>
{
// Board slots are ordered Easy / Medium / Hard (same convention as ClaimActiveContract).
private static readonly string[] Difficulties = ["Easy", "Medium", "Hard"];
private const int RefreshHours = 12;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly Random _rnd = new();
public RequestPlayerContracts(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
public RequestPlayerContracts(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYGetPlayerContractsRequest request)
public Task<FYGetPlayerContractsResult> ExecuteAsync(FYGetPlayerContractsRequest 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>
return Task.FromResult(new FYGetPlayerContractsResult
{
"ContractsActive", "JobBoardsData",
"FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris",
}
);
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value)!;
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value)!;
var titleData = _titleDataService.Find(new List<string> { "Contracts", "Jobs", "LevelData" });
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"])!;
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"])!;
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(titleData["LevelData"])!;
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var lastRefresh = jobBoardsData.LastBoardRefreshTimeUtc?.Seconds ?? 0;
// Initial fill only: populate boards that were seeded empty / never refreshed.
// The periodic rotation is owned by RequestActiveObjectivesAndBoardsData, so we
// don't refresh on a timer here to avoid two competing refresh policies.
var boardsEmpty = jobBoardsData.Boards == null
|| jobBoardsData.Boards.Any(b => b.Contracts == null
|| b.Contracts.Any(c => string.IsNullOrEmpty(c.ContractId)));
if (jobBoardsData.Boards != null && (lastRefresh == 0 || boardsEmpty))
{
foreach (var board in jobBoardsData.Boards)
{
if (board.Contracts == null || !jobs.TryGetValue(board.FactionId, out var factionJobs))
{
continue;
}
var factionKey = "FactionProgression" + board.FactionId;
var factionProgression = userData.TryGetValue(factionKey, out var fp)
? JsonSerializer.Deserialize<int>(fp.Value)
: 0;
var level = levelsData.TryGetValue(board.FactionId, out var levels)
? levels.Length - Array.FindIndex(levels, xp => factionProgression >= xp)
: 0;
for (var idx = 0; idx < board.Contracts.Count && idx < Difficulties.Length; idx++)
{
if (!factionJobs.TryGetValue(Difficulties[idx], out var difficultyJobs))
{
continue;
}
var available = Array.FindAll(difficultyJobs,
jobId => contracts.TryGetValue(jobId, out var c) && level >= c.UnlockData.Level);
if (available.Length > 0)
{
board.Contracts[idx].ContractId = available[_rnd.Next(available.Length)];
}
}
}
jobBoardsData.LastBoardRefreshTimeUtc = new FYTimestamp { Seconds = now };
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string> { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) }
);
}
return new FYGetPlayerContractsResult
{
UserId = userId,
UserId = context.User.FindAuthUserId(),
Error = null,
ActiveContracts = contractsActive.Contracts,
ActiveContracts = new List<FYActiveContractPlayerData>(),
FactionsContracts = new FYFactionsContractsData
{
Boards = jobBoardsData.Boards,
LastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc ?? new FYTimestamp { Seconds = now },
Boards = new List<FYFactionContractsData>
{
new FYFactionContractsData
{
FactionId = "ICA",
Contracts = new List<FYFactionContractData>
{
new FYFactionContractData
{
ContractId = "NEW-Easy-ICA-Gather-1",
},
RefreshHours24UtcFromBackend = RefreshHours,
};
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
});
}
}
@@ -35,11 +35,6 @@ public class RequestStoreRotationDataFunction : ICloudScriptFunction<RequestStor
_httpContextAccessor = httpContextAccessor;
}
// Nombre de variantes de rotation (les storeId "DailyShop_N" / "WeeklyShop_N"
// permettent au client de distinguer chaque rotation et de rafraîchir son cache).
private const int DailyVariants = 8;
private const int WeeklyVariants = 4;
public async Task<RequestStoreRotationDataResponse> ExecuteAsync(RequestStoreRotationDataRequest request)
{
var context = _httpContextAccessor.HttpContext;
@@ -48,33 +43,18 @@ public class RequestStoreRotationDataFunction : ICloudScriptFunction<RequestStor
throw new CloudScriptException("CloudScript was not called within a http request");
}
var now = DateTime.UtcNow;
// Rotation quotidienne : expire au prochain minuit UTC ; l'index de rotation
// est le nombre de jours écoulés depuis l'epoch (déterministe, stable).
var todayUtc = now.Date;
var dailyExpiration = todayUtc.AddDays(1);
var dayIndex = (int)(todayUtc - DateTime.UnixEpoch.Date).TotalDays;
var dailyStoreId = $"DailyShop_{dayIndex % DailyVariants}";
// Rotation hebdomadaire : expire au prochain lundi 00:00 UTC.
// DayOfWeek : dimanche=0 ; on ramène lundi=0.
var daysSinceMonday = ((int)todayUtc.DayOfWeek + 6) % 7;
var weekStart = todayUtc.AddDays(-daysSinceMonday);
var weeklyExpiration = weekStart.AddDays(7);
var weekIndex = (int)Math.Floor((weekStart - DateTime.UnixEpoch.Date).TotalDays / 7);
var weeklyStoreId = $"WeeklyShop_{weekIndex % WeeklyVariants}";
// TODO: Get and save store rotation data
return new RequestStoreRotationDataResponse
{
Error = "",
DailyStoreData = new StoreRotationData {
StoreID = dailyStoreId,
ExpirationData = dailyExpiration
StoreID = "DailyShop",
ExpirationData = DateTime.UtcNow.AddDays(1)
},
WeeklyStoreData = new StoreRotationData {
StoreID = weeklyStoreId,
ExpirationData = weeklyExpiration
StoreID = "WeeklyShop",
ExpirationData = DateTime.UtcNow.AddDays(1)
}
};
}
@@ -1,47 +0,0 @@
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,
};
}
}
@@ -9,9 +9,9 @@ public class RequestUpdateSeasonWipeDataRequest
public class RequestUpdateSeasonWipeDataResponse
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public string UserId;
[JsonPropertyName("error")]
public string Error { get; set; }
public string Error;
}
[CloudScriptFunction("RequestUpdateSeasonWipeData")]
@@ -76,13 +76,6 @@ public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<Reques
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.
// IMPORTANT: The same request is currently used to report the updated inventory in the end of match.
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!request.ItemsToRemove.Contains(item.ItemId)) {
@@ -90,6 +83,7 @@ public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<Reques
}
}
// TODO: Check deleted items to see if other stacks/mods were updated correctly
foreach (var item in request.ItemsToUpdateAmount) {
var inventoryItem = newInventory.Find(i => i.ItemId == item.ItemId);
if (inventoryItem == null) {
@@ -99,7 +93,6 @@ public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<Reques
inventoryItem.ModData = item.ModData;
inventoryItem.PrimaryVanityId = item.PrimaryVanityId;
inventoryItem.SecondaryVanityId = item.SecondaryVanityId;
inventoryItem.Durability = item.Durability;
}
// TODO: Check updated items and validate new items
@@ -6,7 +6,7 @@ public class RegionObject
[JsonPropertyName("region")]
public string Region { get; set; }
[JsonPropertyName("ping")]
public long Ping { get; set; } // client sends 2147483648 (int.MaxValue+1) for "no ping" -> must be long or deserialization 500s
public int Ping { get; set; }
[JsonPropertyName("instanceType")]
public int InstanceType { get; set; }
}
@@ -1,6 +1,5 @@
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;
@@ -34,7 +33,7 @@ public class SellItemsClientResponse
[JsonPropertyName("scrappedItemIds")]
public HashSet<string> ScrappedItemIDs { get; set; }
[JsonPropertyName("changedItems")]
public List<FYCustomItemInfo> ChangedItems { get; set; }
public FYCustomItemInfo[] ChangedItems { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("playerFactionProgressionData")]
@@ -72,67 +71,40 @@ public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientReque
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var progressionFactionKey = $"FactionProgression{request.FactionID}";
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 balanceData = JsonSerializer.Deserialize<Dictionary<string, int>>(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.
var playerBalance = balanceData["SC"];
HashSet<string> scrappedItemIds = [];
foreach (var itemId in request.IDs) {
var inventoryItem = inventory.Find(i => i.ItemId == itemId);
if (inventoryItem == null) {
var inventoryItemIdx = inventory.FindIndex(i => i.ItemId == itemId);
if (inventoryItemIdx == -1) {
continue;
}
var inventoryItem = inventory[inventoryItemIdx];
var blueprintData = blueprints[inventoryItem.BaseItemId];
// NOTE: Ammo and consumables have reverse price mapping.
if (blueprintData.Kind == "Ammo" || blueprintData.Kind == "Ability") {
factionProgression += (int)((float)blueprintData.OverrideScrappingReputation / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
balance.SoftCurrency += (int)((float)blueprintData.OverrideScrappingReturns / blueprintData.MaxAmountPerStack * inventoryItem.Amount);
} else if (blueprintData.Kind == "Armor") {
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
);
} else {
// TODO: Probably better to decide based on item kind instead
if (inventoryItem.Durability == -1) {
factionProgression += blueprintData.OverrideScrappingReputation * inventoryItem.Amount;
balance.SoftCurrency += blueprintData.OverrideScrappingReturns * inventoryItem.Amount;
playerBalance += blueprintData.OverrideScrappingReturns * inventoryItem.Amount;
} else {
// Items that have durability are not stackable so it's always 1 item
factionProgression += MapValue.Map(
inventoryItem.Durability,
blueprintData.DurabilityMax, 0,
blueprintData.OverrideScrappingReputation, (int)(blueprintData.OverrideScrappingReputation * blueprintData.DurabilityBrokenScrappingReturnModifier)
);
playerBalance += MapValue.Map(
inventoryItem.Durability,
blueprintData.DurabilityMax, 0,
blueprintData.OverrideScrappingReturns, (int)(blueprintData.OverrideScrappingReturns * blueprintData.DurabilityBrokenScrappingReturnModifier)
);
}
scrappedItemIds.Add(itemId);
@@ -146,11 +118,13 @@ public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientReque
}
}
balanceData["SC"] = playerBalance;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(newInventory),
["Balance"] = JsonSerializer.Serialize(balance),
["Balance"] = JsonSerializer.Serialize(balanceData),
[progressionFactionKey] = JsonSerializer.Serialize(factionProgression),
}
);
@@ -161,14 +135,14 @@ public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientReque
Error = "",
ChangedCurrencies = [new FYCurrencyItem {
CurrencyName = "SoftCurrency",
Amount = balance.SoftCurrency,
Amount = playerBalance,
}],
PlayerFactionProgressionData = new FYPlayerFactionProgressData {
FactionID = request.FactionID,
CurrentProgression = factionProgression,
},
ScrappedItemIDs = scrappedItemIds,
ChangedItems = changedItems,
ChangedItems = [], // TODO: Changed items from InventoryUpdateData
};
}
}
@@ -47,18 +47,10 @@ public class SetCharacterVanityArchetype : ICloudScriptFunction<FYSetActiveChara
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.BootsItem.ID = "StarterOutfit01_Boots_" + genderSuffix;
returnVanity.ChestItem.ID = "StarterOutfit01_Chest_" + genderSuffix;
returnVanity.GloveItem.ID = "StarterOutfit01_Gloves_" + genderSuffix;
returnVanity.BaseSuitItem.ID = "StarterOutfit01" + genderSuffix + "_BaseSuit";
returnVanity.BodyType = request.ArcheTypeId.EndsWith("G02") ? 1 : 2;
await _userDataService.UpdateAsync(
userId, userId,
@@ -1,7 +1,8 @@
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;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
@@ -60,7 +61,7 @@ public class SkipItemCraftingClient : ICloudScriptFunction<FYSkipItemCraftingCli
new List<string>{"CraftingTimer__2022_05_12", "Balance", "Inventory"}
);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var craftingTimer = JsonSerializer.Deserialize<FYItemCurrentlyBeingCrafted>(userData["CraftingTimer__2022_05_12"].Value);
@@ -77,24 +78,24 @@ public class SkipItemCraftingClient : ICloudScriptFunction<FYSkipItemCraftingCli
FYCurrencyItem[] changedCurrency;
if (request.UseOptionalCosts) {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, shopCraftingData.SkipOptionalCraftingMaxCost, 1);
if (balance.SoftCurrency < remaining) {
if (balance["SC"] < remaining) {
return new FYSkipItemCraftingClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
balance["SC"] -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] }];
} else {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, shopCraftingData.SkipCraftingMaxCost, 1);
if (balance.HardCurrency < remaining) {
if (balance["AU"] < remaining) {
return new FYSkipItemCraftingClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.HardCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
balance["AU"] -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] }];
}
List<FYCustomItemInfo> itemsGrantedOrUpdated = [];
@@ -1,6 +1,5 @@
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;
@@ -70,7 +69,7 @@ public class SkipPlayerQuarterUpgradeClient : ICloudScriptFunction<FYSkipPlayerQ
}
var titleData = _titleDataService.Find(new List<string>{"PlayerQuarters"});
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var playerQuartersData = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
var nextQuartersLevel = playerQuartersData[userPlayerQuarters.Level - 1]; // Quarters level starts from 1
@@ -80,24 +79,24 @@ public class SkipPlayerQuarterUpgradeClient : ICloudScriptFunction<FYSkipPlayerQ
FYCurrencyItem[] changedCurrency;
if (request.UseOptionalCosts) {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nextQuartersLevel.OptionalRushCosts, 1);
if (balance.SoftCurrency < remaining) {
if (balance["SC"] < remaining) {
return new FYSkipPlayerQuarterUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
balance["SC"] -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] }];
} else {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nextQuartersLevel.InitialRushCosts, 1);
if (balance.HardCurrency < remaining) {
if (balance["AU"] < remaining) {
return new FYSkipPlayerQuarterUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.HardCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
balance["AU"] -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] }];
}
var upgradeStartedTime = new FYTimestamp {
@@ -1,6 +1,5 @@
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;
@@ -57,7 +56,7 @@ public class SkipTechTreeNodeUpgradeClient : ICloudScriptFunction<FYSkipTechTree
new List<string>{"TechTreeNodeData", "Balance", "CharacterTechTreeBonuses"}
);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
var currentNode = userTechTree.NodeInProgress;
@@ -82,24 +81,24 @@ public class SkipTechTreeNodeUpgradeClient : ICloudScriptFunction<FYSkipTechTree
FYCurrencyItem[] changedCurrency;
if (request.UseOptionalCosts) {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nodeLevelInfo.OptionalRushCosts, 1);
if (balance.SoftCurrency < remaining) {
if (balance["SC"] < remaining) {
return new FYSkipTechTreeNodeUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
balance["SC"] -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] }];
} else {
var remaining = MapValue.Map(now, craftStartTime, craftEndTime, nodeLevelInfo.InitialRushCosts, 1);
if (balance.HardCurrency < remaining) {
if (balance["AU"] < remaining) {
return new FYSkipTechTreeNodeUpgradeClientResult {
UserID = userId,
Error = "Insufficient balance",
};
}
balance.HardCurrency -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
balance["AU"] -= remaining;
changedCurrency = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] }];
}
userTechTree.TotalUpgrades++;
@@ -1,9 +1,6 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.Database;
using Prospect.Server.Api.Services.Database.Models;
using Prospect.Server.Api.Services.Squad;
using Microsoft.AspNetCore.SignalR;
using Prospect.Server.Api.Hubs;
@@ -31,16 +28,12 @@ public class SquadMemberReadyForMatchFunction : ICloudScriptFunction<SquadMember
private readonly ILogger<SquadMemberReadyForMatchFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHubContext<CycleHub> _hubContext;
private readonly SquadService _squadService;
private readonly DbUserService _userService;
public SquadMemberReadyForMatchFunction(ILogger<SquadMemberReadyForMatchFunction> logger, IHttpContextAccessor httpContextAccessor, IHubContext<CycleHub> hubContext, SquadService squadService, DbUserService userService)
public SquadMemberReadyForMatchFunction(ILogger<SquadMemberReadyForMatchFunction> logger, IHttpContextAccessor httpContextAccessor, IHubContext<CycleHub> hubContext)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_hubContext = hubContext;
_squadService = squadService;
_userService = userService;
}
public async Task<SquadMemberReadyForMatchResponse> ExecuteAsync(SquadMemberReadyForMatchRequest request)
@@ -51,25 +44,27 @@ public class SquadMemberReadyForMatchFunction : ICloudScriptFunction<SquadMember
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var squadId = string.IsNullOrEmpty(request.SquadID) ? userId : request.SquadID;
// Register the caller into the squad (party is formed client-side; each member's client
// calls this with the shared squadId) and record their readiness / selected map.
var displayName = (await _userService.FindByIdAsync(userId))?.DisplayName ?? "";
_squadService.JoinOrCreate(squadId, userId, displayName);
var ready = request.matchmakingSettings?.isReadyForMatch ?? false;
_squadService.SetReady(squadId, userId, ready, request.matchmakingSettings?.selectedMapName);
var squad = _squadService.Get(squadId)!;
var isSquadReady = _squadService.IsSquadReady(squadId);
_logger.LogInformation("SquadMemberReadyForMatch: {User} ready={Ready} in squad {Squad} ({Count} member(s), squadReady={SquadReady})",
userId, ready, squadId, squad.Members.Count, isSquadReady);
return new SquadMemberReadyForMatchResponse
{
Result = 0, // EYSquadActionResult::OK
Squad = _squadService.ToClientSquad(squad),
IsSquadReadyForMatch = isSquadReady,
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,
};
}
}
@@ -1,8 +1,5 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.Squad;
public class SquadMemberSelectedMapRequest
{
[JsonPropertyName("squadId")]
@@ -20,26 +17,15 @@ public class SquadMemberSelectedMapResponse
public class SquadMemberSelectedMapFunction : ICloudScriptFunction<SquadMemberSelectedMapRequest, SquadMemberSelectedMapResponse>
{
private readonly ILogger<SquadMemberSelectedMapFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly SquadService _squadService;
public SquadMemberSelectedMapFunction(ILogger<SquadMemberSelectedMapFunction> logger, IHttpContextAccessor httpContextAccessor, SquadService squadService)
public SquadMemberSelectedMapFunction(ILogger<SquadMemberSelectedMapFunction> logger)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_squadService = squadService;
}
public Task<SquadMemberSelectedMapResponse> ExecuteAsync(SquadMemberSelectedMapRequest request)
public async Task<SquadMemberSelectedMapResponse> ExecuteAsync(SquadMemberSelectedMapRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context != null && !string.IsNullOrEmpty(request.SquadID))
{
var userId = context.User.FindAuthUserId();
_squadService.JoinOrCreate(request.SquadID, userId, "");
_squadService.SetMap(request.SquadID, userId, request.SelectedMapName ?? "");
_logger.LogInformation("SquadMemberSelectedMap: {User} -> {Map} (squad {Squad})", userId, request.SelectedMapName, request.SquadID);
}
return Task.FromResult(new SquadMemberSelectedMapResponse { });
return new SquadMemberSelectedMapResponse
{};
}
}
@@ -1,6 +1,5 @@
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;
@@ -71,9 +70,8 @@ public class StartItemCraftingClient : ICloudScriptFunction<FYStartItemCraftingC
}
// TODO: Contract unlock criteria
// TODO: Inventory limit check
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
@@ -90,14 +88,15 @@ public class StartItemCraftingClient : ICloudScriptFunction<FYStartItemCraftingC
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
remaining -= ingredient.Amount;
balance.SoftCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
balance["SC"] -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] });
} else if (ingredient.Currency == "Aurum") {
remaining -= ingredient.Amount;
balance.HardCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
balance["AU"] -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] });
} else {
foreach (var item in inventory) {
for (var i = 0; i < inventory.Count; i++) {
var item = inventory[i];
if (item.BaseItemId != ingredient.Currency) {
continue;
}
@@ -1,6 +1,5 @@
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;
@@ -67,7 +66,7 @@ public class StartPlayerQuarterUpgradeClient : ICloudScriptFunction<FYStartPlaye
}
var titleData = _titleDataService.Find(new List<string>{"PlayerQuarters"});
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(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
@@ -83,14 +82,15 @@ public class StartPlayerQuarterUpgradeClient : ICloudScriptFunction<FYStartPlaye
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
remaining -= ingredient.Amount;
balance.SoftCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
balance["SC"] -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] });
} else if (ingredient.Currency == "Aurum") {
remaining -= ingredient.Amount;
balance.HardCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
balance["AU"] -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] });
} else {
foreach (var item in inventory) {
for (var i = 0; i < inventory.Count; i++) {
var item = inventory[i];
if (item.BaseItemId != ingredient.Currency) {
continue;
}
@@ -135,7 +135,7 @@ public class StartPlayerQuarterUpgradeClient : ICloudScriptFunction<FYStartPlaye
userId, userId,
new Dictionary<string, string>{
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(newInventory),
["Inventory"] = JsonSerializer.Serialize(inventory),
["PlayerQuartersLevel"] = JsonSerializer.Serialize(userPlayerQuarters),
}
);
@@ -1,6 +1,5 @@
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;
@@ -54,7 +53,7 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTr
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"TechTreeNodeData", "Balance", "Inventory", "PlayerQuartersLevel"}
new List<string>{"TechTreeNodeData", "Balance", "Inventory"}
);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
@@ -68,7 +67,7 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTr
};
}
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var balance = JsonSerializer.Deserialize<Dictionary<string, int>>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes"});
@@ -79,15 +78,7 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTr
var techTreeNodeData = techTreeNodes[request.NodeID];
var nextTreeNodeLevel = techTreeNodeData.PerkLevels[currentLevel]; // Tech tree node levels start from 0
// Dependency check: the player's quarters must be high enough to unlock this node.
var playerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
if (playerQuarters != null && playerQuarters.Level < techTreeNodeData.PlayerQuarterLevelRequired) {
return new FYStartTechTreeNodeUpgradeClientResult
{
UserID = userId,
Error = $"Requires player quarters level {techTreeNodeData.PlayerQuarterLevelRequired}",
};
}
// TODO: Dependency check
// Validate price
// TODO: Refactor
@@ -98,14 +89,15 @@ public class StartTechTreeNodeUpgradeClient : ICloudScriptFunction<FYStartTechTr
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
remaining -= ingredient.Amount;
balance.SoftCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
balance["SC"] -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance["SC"] });
} else if (ingredient.Currency == "Aurum") {
remaining -= ingredient.Amount;
balance.HardCurrency -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
balance["AU"] -= ingredient.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance["AU"] });
} else {
foreach (var item in inventory) {
for (var i = 0; i < inventory.Count; i++) {
var item = inventory[i];
if (item.BaseItemId != ingredient.Currency) {
continue;
}
@@ -1,8 +1,5 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.Database;
using Prospect.Server.Api.Services.Squad;
public class TryGetCompleteSquadInfoRequest
{
@@ -54,30 +51,26 @@ public class TryGetCompleteSquadInfoFunction : ICloudScriptFunction<TryGetComple
{
private readonly ILogger<TryGetCompleteSquadInfoFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly SquadService _squadService;
public TryGetCompleteSquadInfoFunction(ILogger<TryGetCompleteSquadInfoFunction> logger, IHttpContextAccessor httpContextAccessor, SquadService squadService)
public TryGetCompleteSquadInfoFunction(ILogger<TryGetCompleteSquadInfoFunction> logger, IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_logger = logger;
_squadService = squadService;
}
public Task<FYPlayFabSquad> ExecuteAsync(TryGetCompleteSquadInfoRequest request)
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();
var squad = _squadService.GetByUser(userId);
if (squad == null)
// var userId = context.User.FindAuthUserId();
return new FYPlayFabSquad
{
// Not in a squad yet — return an empty squad (same as the previous stub).
return Task.FromResult(new FYPlayFabSquad());
}
_logger.LogInformation("TryGetCompleteSquadInfo: user {User} in squad {Squad} ({Count} member(s))", userId, squad.SquadId, squad.Members.Count);
return Task.FromResult(_squadService.ToClientSquad(squad));
// SquadID = "100",
// Members = [],
};
}
}
@@ -1,62 +0,0 @@
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;
// Custom request for dedicated server implementation
public class FYUpdatePlayerActiveContractsRequest {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("contracts")]
public List<FYActiveContractPlayerData> Contracts { get; set; }
}
public class FYUpdatePlayerActiveContractsResult {
}
[CloudScriptFunction("UpdatePlayerActiveContracts")]
public class UpdatePlayerActiveContracts : ICloudScriptFunction<FYUpdatePlayerActiveContractsRequest, FYUpdatePlayerActiveContractsResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public UpdatePlayerActiveContracts(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYUpdatePlayerActiveContractsResult> ExecuteAsync(FYUpdatePlayerActiveContractsRequest 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);
foreach (var contract in request.Contracts) {
var foundContract = contractsActive.Contracts.Find(item => item.ContractID == contract.ContractID);
if (foundContract == null) {
continue;
}
foundContract.Progress = contract.Progress;
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["ContractsActive"] = JsonSerializer.Serialize(contractsActive) }
);
return new FYUpdatePlayerActiveContractsResult
{};
}
}
@@ -1,51 +1,12 @@
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.UserData;
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
// Persisted presence, read back by GetFriendList to show friends' online / in-match state.
public class PlayerPresenceState
{
[JsonPropertyName("inMatch")]
public bool InMatch { get; set; }
[JsonPropertyName("lastSeenUtc")]
public long LastSeenUtc { get; set; }
}
[CloudScriptFunction("UpdatePlayerPresenceState")]
public class UpdatePlayerPresenceState : ICloudScriptFunction<FYUpdatePlayerPresenceStateRequest, object>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public UpdatePlayerPresenceState(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
public Task<object> ExecuteAsync(FYUpdatePlayerPresenceStateRequest request)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<object> ExecuteAsync(FYUpdatePlayerPresenceStateRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new { };
}
var userId = context.User.FindAuthUserId();
var state = new PlayerPresenceState
{
InMatch = request.InMatch,
LastSeenUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
};
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string> { ["PresenceState"] = JsonSerializer.Serialize(state) }
);
return new { };
return Task.FromResult<object>(new { });
}
}
@@ -1,14 +1,7 @@
using System.Text.Json.Serialization;
namespace Prospect.Server.Api.Services.CloudScript.Models.Data;
namespace Prospect.Server.Api.Services.CloudScript.Models.Data;
// Envoyé par le client au déploiement (dans FYEnterMatchAzureFunction) : quels items
// de l'inventaire assurer, avec quelle police (voir TitleData "InventoryInsurance").
public class FYPurchaseInventoryInsuranceRequest
{
[JsonPropertyName("insuranceId")]
public string InsuranceId { get; set; }
[JsonPropertyName("itemIds")]
public List<string> ItemIds { get; set; }
// m_insuranceId
// m_itemIds
}
@@ -29,30 +29,3 @@ public class FYEnterMatchAzureFunction
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class FYEnterMatchAzureFunctionCB
{
[JsonPropertyName("optimalRegion")]
public string? OptimalRegion { get; set; }
[JsonPropertyName("isMatch")]
public bool IsMatch { get; set; }
[JsonPropertyName("bypassMaintenanceMode")]
public bool BypassMaintenanceMode { get; set; }
[JsonPropertyName("debugOption")]
public string? DebugOption { get; set; }
[JsonPropertyName("mapName")]
public string? MapName { get; set; }
[JsonPropertyName("squad_id")]
public string? SquadId { get; set; }
[JsonPropertyName("purchaseInsuranceRequest")]
public FYPurchaseInventoryInsuranceRequest PurchaseInventoryInsurances { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
@@ -21,30 +21,3 @@ public class FYEnterMatchmakingResult
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}
public class FYEnterMatchAzureFunctionResult
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("address")]
public string? Address { get; set; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; set; }
[JsonPropertyName("port")]
public int Port { get; set; }
[JsonPropertyName("sharedIndex")]
public int ShardIndex { get; set; }
[JsonPropertyName("singleplayerStation")]
public bool SingleplayerStation { get; set; }
[JsonPropertyName("maintenanceMode")]
public bool MaintenanceMode { get; set; }
[JsonPropertyName("sessionTimeJoinDelay")]
public float SessionTimeJoinDelay { get; set; }
}
@@ -16,12 +16,6 @@ public class DbEntityService : BaseDbService<PlayFabEntity>
return await Collection.Find(user => user.UserId == userId).SingleOrDefaultAsync();
}
// Admin back-office: list every entity (title_player_account).
public async Task<List<PlayFabEntity>> GetAllAsync()
{
return await Collection.Find(_ => true).ToListAsync();
}
private async Task<PlayFabEntity> CreateAsync(string userId)
{
var user = new PlayFabEntity
@@ -46,19 +46,6 @@ public class DbUserDataService : BaseDbService<PlayFabUserData>
return await query.ToCursorAsync();
}
// Admin back-office: every key/value stored for a player.
public async Task<List<PlayFabUserData>> GetAllForUserAsync(string playFabId)
{
return await Collection.Find(data => data.PlayFabId == playFabId)
.SortBy(data => data.Key).ToListAsync();
}
// Admin back-office: total stored key/value documents.
public async Task<long> CountAsync()
{
return await Collection.CountDocumentsAsync(_ => true);
}
public async Task UpdateValueAsync(string dataId, string value)
{
var update = Builders<PlayFabUserData>.Update
@@ -18,26 +18,6 @@ public class DbUserService : BaseDbService<PlayFabUser>
auth.Key == key)).FirstOrDefaultAsync();
}
// Resolve a player by its PlayFab id (used to display squad member names).
public async Task<PlayFabUser> FindByIdAsync(string id)
{
return await Collection.Find(user => user.Id == id).FirstOrDefaultAsync();
}
// Admin back-office: list every player.
public async Task<List<PlayFabUser>> GetAllAsync()
{
return await Collection.Find(_ => true).ToListAsync();
}
// Persist a display-name change (UpdateUserTitleDisplayName). Without this the rename
// is echoed back to the client but lost on next login.
public async Task UpdateDisplayNameAsync(string id, string displayName)
{
var update = Builders<PlayFabUser>.Update.Set(user => user.DisplayName, displayName);
await Collection.UpdateOneAsync(user => user.Id == id, update);
}
private async Task<PlayFabUser> CreateAsync(PlayFabUserAuthType type, string key)
{
var user = new PlayFabUser
@@ -63,18 +43,4 @@ public class DbUserService : BaseDbService<PlayFabUser>
return await FindAsync(type, key) ??
await CreateAsync(type, key);
}
// Resolve several auth keys (e.g. a batch of Steam IDs) to the players that exist on
// this server in a single query. Used by the friends list to keep only the imported
// Steam friends who have actually played here.
public async Task<List<PlayFabUser>> FindManyAsync(PlayFabUserAuthType type, IReadOnlyCollection<string> keys)
{
if (keys == null || keys.Count == 0)
{
return new List<PlayFabUser>();
}
var wanted = new HashSet<string>(keys);
return await Collection.Find(user => user.Auth.Any(auth =>
auth.Type == type && wanted.Contains(auth.Key))).ToListAsync();
}
}
@@ -1,99 +0,0 @@
using System.Collections.Concurrent;
namespace Prospect.Server.Api.Services.Squad;
// In-memory squad/lobby state. Squads are keyed by the squadId the client provides on the
// SquadMember* calls (the client forms the party and shares a squadId; each member's client
// then calls the backend with it). A member is (lazily) registered into that squad on any
// squad call, and the current squad for a user is tracked so TryGetCompleteSquadInfo — which
// carries no squadId — can still return it. State is process-local (fine for a small server).
public class SquadMemberInfo
{
public string UserId { get; set; } = "";
public string DisplayName { get; set; } = "";
public int OnlineState { get; set; }
public bool IsReady { get; set; }
public string SelectedMap { get; set; } = "";
public bool IsLeader { get; set; }
}
public class SquadInfo
{
public string SquadId { get; set; } = "";
public ConcurrentDictionary<string, SquadMemberInfo> Members { get; } = new();
}
public class SquadService
{
private readonly ConcurrentDictionary<string, SquadInfo> _squads = new();
private readonly ConcurrentDictionary<string, string> _userToSquad = new();
public SquadInfo JoinOrCreate(string squadId, string userId, string displayName)
{
var squad = _squads.GetOrAdd(squadId, id => new SquadInfo { SquadId = id });
var isFirst = squad.Members.IsEmpty;
var member = squad.Members.GetOrAdd(userId, uid => new SquadMemberInfo { UserId = uid, IsLeader = isFirst });
if (!string.IsNullOrEmpty(displayName)) member.DisplayName = displayName;
_userToSquad[userId] = squadId;
return squad;
}
public SquadInfo? GetByUser(string userId)
=> _userToSquad.TryGetValue(userId, out var sid) && _squads.TryGetValue(sid, out var sq) ? sq : null;
public SquadInfo? Get(string squadId)
=> _squads.TryGetValue(squadId, out var sq) ? sq : null;
public void SetReady(string squadId, string userId, bool ready, string? selectedMap)
{
if (_squads.TryGetValue(squadId, out var sq) && sq.Members.TryGetValue(userId, out var m))
{
m.IsReady = ready;
if (!string.IsNullOrEmpty(selectedMap)) m.SelectedMap = selectedMap;
}
}
public void SetMap(string squadId, string userId, string map)
{
if (_squads.TryGetValue(squadId, out var sq) && sq.Members.TryGetValue(userId, out var m))
m.SelectedMap = map;
}
public bool IsSquadReady(string squadId)
=> _squads.TryGetValue(squadId, out var sq) && !sq.Members.IsEmpty && sq.Members.Values.All(m => m.IsReady);
public void Leave(string userId)
{
if (_userToSquad.TryRemove(userId, out var sid) && _squads.TryGetValue(sid, out var sq))
{
sq.Members.TryRemove(userId, out _);
if (sq.Members.IsEmpty) _squads.TryRemove(sid, out _);
}
}
// Build the client-facing squad object (models live in the global namespace).
public FYPlayFabSquad ToClientSquad(SquadInfo squad)
{
return new FYPlayFabSquad
{
SquadID = squad.SquadId,
Members = squad.Members.Values.Select(m => new FYPlayFabSquadMember
{
Profile = new FYPlayFabPlayerProfile
{
PlayerId = m.UserId,
DisplayName = m.DisplayName,
AvatarUrl = "",
},
onlineState = m.OnlineState,
matchmakingSettings = new FYUserMatchmakingSettings
{
isReadyForMatch = m.IsReady,
selectedMapName = m.SelectedMap,
isSecretLeader = m.IsLeader,
},
mapRowNamesUnlocked = Array.Empty<string>(),
}).ToArray(),
};
}
}
@@ -1,55 +0,0 @@
using System.Text.Json;
namespace Prospect.Server.Api.Services.Steam;
// Resolves a Steam persona (display) name from a SteamID64 via the Steam Web API.
// PlayFab does this server-side too; our emulator only gets the SteamID from the auth
// ticket, so the real name needs an API key. Inert (no-op) when no key is configured.
public class SteamWebApiService
{
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(5) };
private readonly string? _apiKey;
private readonly ILogger<SteamWebApiService> _logger;
public SteamWebApiService(IConfiguration configuration, ILogger<SteamWebApiService> logger)
{
_apiKey = configuration["SteamWebApiKey"];
_logger = logger;
}
public bool Enabled => !string.IsNullOrWhiteSpace(_apiKey);
public async Task<string?> GetPersonaNameAsync(string steamId64)
{
if (!Enabled)
{
return null;
}
try
{
var url = $"https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key={_apiKey}&steamids={steamId64}";
var json = await Http.GetStringAsync(url);
using var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("response", out var response) &&
response.TryGetProperty("players", out var players) &&
players.ValueKind == JsonValueKind.Array)
{
foreach (var player in players.EnumerateArray())
{
if (player.TryGetProperty("personaname", out var name))
{
return name.GetString();
}
}
}
}
catch (Exception e)
{
_logger.LogWarning(e, "Steam GetPlayerSummaries failed for {SteamId}", steamId64);
}
return null;
}
}
File diff suppressed because one or more lines are too long
@@ -1,9 +1,6 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.CloudScript.Functions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.Database;
namespace Prospect.Server.Api.Services.UserData;
@@ -73,53 +70,35 @@ public class UserDataService
};
}
var defaultVanity = new FYCharacterVanity {
UserID = playFabId,
ArchetypeID = "E01_G02",
SlotIndex = 0,
BaseSuitItem = new FYVanityMaterialItem {
ID = "StarterOutfit01M_BaseSuit",
MaterialIndex = 0,
},
HeadItem = new FYVanityMaterialItem {
ID = "E01_G02_Head01",
MaterialIndex = 0,
},
MeleeWeaponItem = new FYVanityMaterialItem {
ID = "Melee_Omega",
MaterialIndex = 0,
},
BodyType = 1,
};
#if SEASON_2_RELEASE || SEASON_2_DEBUG
defaultVanity.BootsItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Boots_M",
MaterialIndex = 0,
};
defaultVanity.ChestItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Chest_M",
MaterialIndex = 0,
};
defaultVanity.GloveItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Gloves_M",
MaterialIndex = 0,
};
#elif SEASON_3_RELEASE || SEASON_3_DEBUG
defaultVanity.BootsItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Boots",
MaterialIndex = 0,
};
defaultVanity.ChestItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Chest",
MaterialIndex = 0,
};
defaultVanity.GloveItem = new FYVanityMaterialItem {
ID = "StarterOutfit01_Gloves",
MaterialIndex = 0,
};
#else
#error Unsupported build type
#endif
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
// TODO: For testing purposes
List<FYCustomItemInfo> inventory = new List<FYCustomItemInfo>();
foreach (var dict in blueprints)
{
var blueprint = dict.Value;
if (blueprint.Kind != "Material")
{
continue;
}
for (int i = 0; i < 3; i++)
{
inventory.Add(new FYCustomItemInfo {
Amount = 10000,
BaseItemId = blueprint.Name,
Durability = blueprint.DurabilityMax,
ItemId = Guid.NewGuid().ToString(),
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
});
}
}
// TODO: Proper objects.
var defaultData = new Dictionary<string, (bool isPublic, string value)>
@@ -171,27 +150,10 @@ public class UserDataService
// Used to optimize bonuses calculation and further validations without getting the user data and computing bonuses in runtime.
["CharacterTechTreeBonuses"] = (false, "{\"aurumCap\":0,\"aurumRate\":0,\"kmarksCap\":0,\"kmarksRate\":0,\"crateTier\":0,\"stashSize\":0,\"safePocketSize\":0,\"upgradeSpeed\":1.0}"),
["GlobalVanity"] = (false, $"{{\"activeGlobalVanityIds\":[\"Season03_Spray_13\",\"Banner_MIne\",\"Emote_Chill_01\",\"\",\"\",\"\",\"\",\"\"], \"droppodId\": \"VDP_Omega02\"}}"),
["CharacterVanity"] = (false, JsonSerializer.Serialize(defaultVanity)),
["Inventory"] = (false, "[]"),
["JobBoardsData"] = (false, JsonSerializer.Serialize(
new JobBoardsData {
LastBoardRefreshTimeUtc = new CloudScript.Models.Data.FYTimestamp { Seconds = 0 },
Boards = [
new FYFactionContractsData {
FactionId = "Korolev",
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
},
new FYFactionContractsData {
FactionId = "Osiris",
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
},
new FYFactionContractsData {
FactionId = "ICA",
Contracts = [ new FYFactionContractData { }, new FYFactionContractData { }, new FYFactionContractData { }]
}
]
}
)),
// NOTE: Default CharacterVanity is based on Season 2 IDs.
["CharacterVanity"] = (false, $"{{\"userId\":\"{playFabId}\",\"head_item\":{{\"id\":\"E01_G02_Head01\",\"materialIndex\":0}},\"boots_item\":{{\"id\":\"StarterOutfit01_Boots_M\",\"materialIndex\":0}},\"chest_item\":{{\"id\":\"StarterOutfit01_Chest_M\",\"materialIndex\":0}},\"glove_item\":{{\"id\":\"StarterOutfit01_Gloves_M\",\"materialIndex\":0}},\"base_suit_item\":{{\"id\":\"StarterOutfit01M_BaseSuit\",\"materialIndex\":0}},\"melee_weapon_item\":{{\"id\":\"Melee_Omega\",\"materialIndex\":0}},\"body_type\":1,\"archetype_id\":\"E01_G02\",\"slot_index\":0}}"),
// ["Inventory"] = (false, "[]"),
["Inventory"] = (false, JsonSerializer.Serialize(inventory)),
["VanityItems"] = (false, "[]"),
["RetentionBonus"] = (false, "{\"claimedAll\":false,\"daysClaimed\":0,\"lastClaimTime\":{\"seconds\":0}}"),
["Balance"] = (false, "{\"IN\": 0,\"AU\": 800,\"SC\": 30000}"),
@@ -233,6 +195,9 @@ public class UserDataService
}
var other = currentUserId != requestUserId;
if (other) {
Console.WriteLine("aboba");
}
var result = new Dictionary<string, FUserDataRecord>();
if (keys != null && keys.Count > 0)
+1 -6
View File
@@ -30,16 +30,11 @@ public class Startup
services.AddSingleton<UserDataService>();
services.AddSingleton<TitleDataService>();
services.AddSingleton<Prospect.Server.Api.Services.Steam.SteamWebApiService>();
services.AddSingleton<DbUserService>();
services.AddSingleton<DbEntityService>();
services.AddSingleton<DbUserDataService>();
services.AddSingleton<Prospect.Server.Api.Services.Squad.SquadService>();
services.AddSingleton<Prospect.Server.Api.Hubs.SignalRConnectionRegistry>();
services.AddHostedService<QosService>();
services.AddSingleton<QosServer>();
@@ -70,7 +65,7 @@ public class Startup
app.UseDeveloperExceptionPage();
}
// app.UseHttpsRedirection();
app.UseHttpsRedirection();
app.UseSerilogRequestLogging();
app.UseMiddleware<RequestLoggerMiddleware>();
app.UseRouting();
+6 -3
View File
@@ -8,11 +8,14 @@
},
"PlayFabSettings": {
"PublisherId": "850902E5B40508ED",
"TitleId": "2EA46",
"SignalRURL": "https://127.0.0.1:8443/signalr/?hub=pubsub",
"SignalRAccessToken": "TEST"
"TitleId": "2EA46"
},
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://2EA46.playfabapi.com"
}
},
"Certificates": {
"Default": {
"Path": ".\\certificate.pfx"
@@ -5,7 +5,6 @@
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
<ItemGroup>
-1
View File
@@ -3,7 +3,6 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>true</ImplicitUsings>
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
</Project>
@@ -4,7 +4,6 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<IsRoslynComponent>true</IsRoslynComponent>
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
<ItemGroup>
@@ -5,8 +5,6 @@
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
<ItemGroup>
@@ -5,7 +5,6 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release;Season 3 Release;Season 2 Release;Season 2 Debug;Season 3 Debug</Configurations>
</PropertyGroup>
<PropertyGroup>
+1 -185
View File
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.12.35514.174
VisualStudioVersion = 17.12.35514.174 d17.12
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Server.Api", "Prospect.Server.Api\Prospect.Server.Api.csproj", "{A539B020-8BEC-497F-9176-DC2665D927A1}"
EndProject
@@ -21,26 +21,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Tests", "Pr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Unreal.Generator", "Prospect.Unreal.Generator\Prospect.Unreal.Generator.csproj", "{71E3E262-49C5-4B6C-9670-D0741CEDB63B}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Client.Loader", "Prospect.Client.Loader\Prospect.Client.Loader.vcxproj", "{04B1D963-2BCB-4887-9D6D-04CB3366A348}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Prospect.Agent", "Prospect.Agent\Prospect.Agent.vcxproj", "{A9BA7D25-F239-4320-A0DC-85E8001FD669}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prospect.Client.Config", "Prospect.Client.Config\Prospect.Client.Config.csproj", "{6DECEB07-996B-4EBC-927C-13E170161107}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
Season 2 Debug|x64 = Season 2 Debug|x64
Season 2 Debug|x86 = Season 2 Debug|x86
Season 2 Release|x64 = Season 2 Release|x64
Season 2 Release|x86 = Season 2 Release|x86
Season 3 Debug|x64 = Season 3 Debug|x64
Season 3 Debug|x86 = Season 3 Debug|x86
Season 3 Release|x64 = Season 3 Release|x64
Season 3 Release|x86 = Season 3 Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A539B020-8BEC-497F-9176-DC2665D927A1}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -51,22 +37,6 @@ Global
{A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x64.Build.0 = Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x86.ActiveCfg = Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Release|x86.Build.0 = Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{A539B020-8BEC-497F-9176-DC2665D927A1}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Debug|x64.ActiveCfg = Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Debug|x64.Build.0 = Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -75,22 +45,6 @@ Global
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Release|x64.Build.0 = Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Release|x86.ActiveCfg = Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Release|x86.Build.0 = Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{E65720EB-7ED5-4D81-BE06-1DFFD57C1C47}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Debug|x64.ActiveCfg = Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Debug|x64.Build.0 = Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -99,22 +53,6 @@ Global
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Release|x64.Build.0 = Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Release|x86.ActiveCfg = Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Release|x86.Build.0 = Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{DE064B5C-5E9A-44FB-8DB0-5E61DD8ED4F6}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Debug|x64.ActiveCfg = Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Debug|x64.Build.0 = Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -123,22 +61,6 @@ Global
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x64.Build.0 = Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.ActiveCfg = Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Release|x86.Build.0 = Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{380DD490-8F01-4025-91D7-74C289AA5B75}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x64.ActiveCfg = Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x64.Build.0 = Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -147,22 +69,6 @@ Global
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x64.Build.0 = Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.ActiveCfg = Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Release|x86.Build.0 = Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x64.ActiveCfg = Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x64.Build.0 = Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -171,94 +77,6 @@ Global
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x64.Build.0 = Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x86.ActiveCfg = Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Release|x86.Build.0 = Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{71E3E262-49C5-4B6C-9670-D0741CEDB63B}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x64.ActiveCfg = Debug|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x64.Build.0 = Debug|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x86.ActiveCfg = Debug|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Debug|x86.Build.0 = Debug|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x64.ActiveCfg = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x64.Build.0 = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x86.ActiveCfg = Release|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Release|x86.Build.0 = Release|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x64.ActiveCfg = Debug|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x64.Build.0 = Debug|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x86.ActiveCfg = Debug|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Debug|x86.Build.0 = Debug|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x64.ActiveCfg = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x64.Build.0 = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x86.ActiveCfg = Release|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 2 Release|x86.Build.0 = Release|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x64.ActiveCfg = Debug|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x64.Build.0 = Debug|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x86.ActiveCfg = Debug|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Debug|x86.Build.0 = Debug|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x64.ActiveCfg = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x64.Build.0 = Release|x64
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.ActiveCfg = Release|Win32
{04B1D963-2BCB-4887-9D6D-04CB3366A348}.Season 3 Release|x86.Build.0 = Release|Win32
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x64.ActiveCfg = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x64.Build.0 = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x86.ActiveCfg = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Debug|x86.Build.0 = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x64.ActiveCfg = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x64.Build.0 = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x86.ActiveCfg = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Release|x86.Build.0 = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x64.ActiveCfg = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x64.Build.0 = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x86.ActiveCfg = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Debug|x86.Build.0 = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x64.ActiveCfg = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x64.Build.0 = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x86.ActiveCfg = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 2 Release|x86.Build.0 = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x64.ActiveCfg = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x64.Build.0 = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x86.ActiveCfg = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Debug|x86.Build.0 = Debug|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.ActiveCfg = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x64.Build.0 = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.ActiveCfg = Release|x64
{A9BA7D25-F239-4320-A0DC-85E8001FD669}.Season 3 Release|x86.Build.0 = Release|x64
{6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x64.ActiveCfg = Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x64.Build.0 = Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x86.ActiveCfg = Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Debug|x86.Build.0 = Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Release|x64.ActiveCfg = Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Release|x64.Build.0 = Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Release|x86.ActiveCfg = Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Release|x86.Build.0 = Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x64.ActiveCfg = Season 2 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x64.Build.0 = Season 2 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x86.ActiveCfg = Season 2 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Debug|x86.Build.0 = Season 2 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x64.ActiveCfg = Season 2 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x64.Build.0 = Season 2 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x86.ActiveCfg = Season 2 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 2 Release|x86.Build.0 = Season 2 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x64.ActiveCfg = Season 3 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x64.Build.0 = Season 3 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x86.ActiveCfg = Season 3 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Debug|x86.Build.0 = Season 3 Debug|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x64.ActiveCfg = Season 3 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x64.Build.0 = Season 3 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x86.ActiveCfg = Season 3 Release|Any CPU
{6DECEB07-996B-4EBC-927C-13E170161107}.Season 3 Release|x86.Build.0 = Season 3 Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -270,8 +88,6 @@ Global
{380DD490-8F01-4025-91D7-74C289AA5B75} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
{14DAE1A4-6F3F-43C0-AC17-9DCDE420AF34} = {168B6247-2D33-4942-AB3F-C5041B3BDFEA}
{71E3E262-49C5-4B6C-9670-D0741CEDB63B} = {76364BAA-BC9D-4A92-AA2A-8914FAE22834}
{04B1D963-2BCB-4887-9D6D-04CB3366A348} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
{A9BA7D25-F239-4320-A0DC-85E8001FD669} = {31D3D5D2-B011-4F9B-B6CC-D0A35675D797}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {12888C62-D3A2-4DD7-A9A5-1481D95865D0}
+1 -6
View File
@@ -4,13 +4,8 @@ from cryptography.x509 import NameOID, CertificateBuilder
from cryptography import x509
import datetime
import os
import argparse
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--common-name", type=str, default='127.0.0.1')
args = parser.parse_args()
if os.path.exists("certificate.pfx"):
q = input("certificate.pfx already exists. Are you sure you want to generate a new certificate? (y/n): ")
if q != "y":
@@ -25,7 +20,7 @@ def main():
# Generate self-signed certificate
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, args.common_name),
x509.NameAttribute(NameOID.COMMON_NAME, '2EA46.playfabapi.com'),
])
cert = CertificateBuilder().subject_name(