58 Commits
Author SHA1 Message Date
neckfire c6b2330e1d docs: pointer vers la release pour LoaderPack + cert
Build & Deploy / build (push) Successful in 16s
2026-07-14 13:09:57 +00:00
neckfire 9737316dbc docs: ajoute la commande download_depot + astuce disque
Build & Deploy / build (push) Successful in 19s
2026-07-14 13:07:35 +00:00
neckfireandClaude Opus 4.8 a4e006f84f docs: guide d installation client pour les amis (serveur prive)
Build & Deploy / build (push) Successful in 15s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:33:37 +00:00
neckfire 3cdf8bc7e2 Merge pull request 'Boutiques : GetStoreItems (vendeurs + Aurum) + fix vanities possédées' (#2) from preprod into main
Build & Deploy / build (push) Successful in 13s
2026-07-14 12:07:18 +00:00
neckfireandClaude Opus 4.8 5d93b3541d fix(store): return per-user owned vanities, not the whole catalogue
Build & Deploy / build (push) Successful in 23s
GetUserInventory returned every vanity in the Vanities title data as an
owned item, so the client showed all Aurum-shop cosmetics as already
owned. Return only the cosmetics the player actually owns: purchased
(VanityItems) plus currently equipped (CharacterVanity slots and the
GlobalVanity sprays / banner / emotes / drop pod), so the starter outfit
stays owned while the rest of the Aurum shop becomes purchasable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:55:50 +02:00
neckfireandClaude Opus 4.8 40dad82559 feat(store): populate Aurum/cosmetic shops from Vanities
Build & Deploy / build (push) Successful in 23s
GetStoreItems now also serves the cosmetic stores the client queries
(AurumShop, and the DailyShop_/WeeklyShop_ rotations) from the Vanities
title data: every vanity with StoreData.Amount > 0 is offered for that
price in Aurum (AU). AurumShop lists the full priced catalogue (632
items); the daily/weekly ids show a deterministic rotating slice so they
stay stable within a period but change across periods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:46:17 +02:00
neckfireandClaude Opus 4.8 3b108d99ca feat(store): populate shops via GetStoreItems
Build & Deploy / build (push) Successful in 23s
GetStoreItems was a stub returning an empty object, so every shop
(Korolev / ICA / Osiris / QuickShop vendors) showed up empty. Build the
store on the fly from the Blueprints title data: for the requested
StoreId, list every item that declares an ItemShopsCraftingData entry
for that store, converting its currency recipe ingredients into
VirtualCurrencyPrices (SoftCurrency->SC, Aurum->AU, InsuranceCurrency->IN).
Item-ingredient (crafting) entries are left to the crafting UI.

Adds the StoreId/CatalogVersion fields to the request and the
FGetStoreItemsResult / FStoreItem PlayFab response models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:53:37 +02:00
neckfire 67f3095896 Merge pull request 'Promotion preprod → prod : contrats/kills, factions, tech-tree, assurance payout, Fortuna Pass' (#1) from preprod into main
Build & Deploy / build (push) Successful in 11s
2026-07-14 10:33:31 +00:00
neckfireandClaude Opus 4.8 89e7a69049 feat(insurance,fortuna): payout on death + Fortuna Pass claim safety
Build & Deploy / build (push) Successful in 21s
- CompleteInventoryUpdate: insured items lost in a raid (present in
  ItemsToRemove) are recovered back to the stash and the insurance is
  consumed. This is the payout counterpart to the premium already
  debited on deploy (EnterMatchmakingStation). Gated on the policy
  declaring a positive Payout; never fails the inventory commit.
- RequestClaimFortunaPassRewards: log the client-sent reward IDs so a
  real FortunaPass{2,3}_Rewards catalog can be built to match, and stop
  recording a reward as claimed when there is no catalog entry to grant
  (previously it was burned permanently and became unclaimable).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 12:30:11 +02:00
neckfireandClaude Opus 4.8 6f3410b37d fix(contracts): auto-credit kill objectives in client-hosted raids
Build & Deploy / build (push) Successful in 21s
Kill-type contract objectives were never persisted: the client-hosted
raid has no dedicated game server to report per-kill progress, so
creature kills stayed at 0 and contracts reported "no kills" at claim.

- EnterMatchmakingMatch: auto-credit Kills objectives to MaxProgress on
  deploy (mirrors the item-ownership pre-compute already done there and
  the player-kill auto-credit in ActivateContract).
- ClaimActiveContract: treat Kills objectives as met as a safety net for
  contracts activated before deploy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 11:56:14 +02:00
neckfireandClaude Opus 4.8 345a822a2e feat(preprod): contrats fonctionnels, réputation factions, gating tech-tree & shop
Build & Deploy / build (push) Successful in 21s
- RequestPlayerContracts: renvoie les vrais contrats actifs + job boards du joueur
  (peuplement initial des boards vides) au lieu d'une liste hardcodée.
- RequestFactionProgression: renvoie la réputation réelle des 3 factions (au lieu de 0).
- StartTechTreeNodeUpgradeClient: dependency check PlayerQuarterLevelRequired.
- PurchaseWeaponShopItem: critère de déblocage par contrat (UnlockData.ContractLockPurchase).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 02:20:27 +02:00
neckfireandClaude Opus 4.8 1e79360b6e feat: rotation shops (daily/weekly) + items insurance + season pass
Build & Deploy / build (push) Successful in 10s
- RequestStoreRotationData: rotation daily (reset 00:00 UTC) + weekly (lundi) avec
  storeIDs rotatifs déterministes et timers d'expiration corrects.
- EnterMatchmakingStation: traite purchaseInsuranceRequest au déploiement — marque
  les items assurés (Insurance + owner) et débite la prime (TitleData InventoryInsurance).
- FYPurchaseInventoryInsuranceRequest: modèle complété (insuranceId + itemIds).
- RequestClaimFortunaPassRewards: claim saison-correct (S2/S3 séparés, plus d'écrasement)
  + octroi des récompenses depuis catalogue TitleData optionnel (repli sûr sinon).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:55:54 +02:00
neckfireandClaude Opus 4.8 31aa20abd8 fix(ci): publier en configuration 'Season 2 Release' (client S2 déployé)
Build & Deploy / build (push) Successful in 8s
Le code sélectionne la saison via #if SEASON_2_RELEASE ; sans cette config le
build échoue (#error Unsupported build type). Build validé en local.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:37:11 +02:00
neckfireandClaude Opus 4.8 2630a9f2af ci: build serveur API depuis les sources + déploiement registry (prod/preprod)
Build & Deploy / build (push) Failing after 12s
Dockerfile multi-stage (SDK 8 publish → aspnet 8) et workflow Gitea Actions :
main → :latest (prod), preprod → :preprod. Base = tag b8 (état déployé).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 02:33:04 +02:00
Yury d9bd46e6e0 Update README 2025-03-31 01:40:20 +03:00
Yury 269659b8da Stash changes 2025-03-31 01:39:22 +03:00
Yury 14097808d8 Add CompleteInventoryUpdate CloudScript function from closed beta 2025-03-31 01:38:58 +03:00
Yury 6fe4432c6e Add custom UpdatePlayerActiveContracts CloudScript function for contracts update 2025-03-31 01:38:35 +03:00
Yury 4f11c8e550 Change Prospect.Client.Loader output dir 2025-03-31 01:37:34 +03:00
Yury 1ac2374eff Update README 2025-03-31 01:37:16 +03:00
Yury b5cb885262 Improve loader
* Improve loading speed
* Change DLL loading order
* Add Prospect.Agent.dll
2025-03-31 01:37:00 +03:00
Yury d55ef94fd9 Update launchSettings for HTTP server 2025-03-31 01:35:20 +03:00
Yury 1f4640fccd Fix given amount of quest reward 2025-03-31 01:34:41 +03:00
Yury faf90c1841 Autocomplete only PvP quests now 2025-03-31 01:34:16 +03:00
Yury efbacc57d1 Add Prospect.Agent DLL for utility functions 2025-03-31 01:33:41 +03:00
Yury 458585c37f Add "common-name" command-line parameter to generate_ssl 2025-03-31 01:33:01 +03:00
Yury b3c5fa3e2b Disallow accepting already completed contracts 2025-03-31 01:32:15 +03:00
Yury 2fb730cb8c Temporary disable HTTPS redirection 2025-03-31 01:31:46 +03:00
Yury 87e1708d6b Fix inventory update on quarter level upgrade 2025-03-31 01:31:35 +03:00
Yury 2695f25242 Update contract info structure and title data
* Include information about kill conditions
* Use default client settings for MatchmakingSetup
2025-03-31 01:31:18 +03:00
Yury 81dea35778 Allow for configurable SignalR connection settings 2025-03-31 01:30:14 +03:00
Yury 704abac924 Add client version check 2025-03-31 01:29:00 +03:00
Yury 033227eb83 Revert stash size to 90 items 2025-03-25 20:40:24 +02:00
Yury 388dd99f60 Update README 2025-03-25 20:09:22 +02:00
Yury 986db8acb8 Update solution file 2025-03-25 20:09:09 +02:00
Yury 1b94cbdd8d Update TitleData to match client data 2025-03-25 20:07:29 +02:00
Yury b2e2106912 Fix insurance token name 2025-03-25 20:06:35 +02:00
Yury 3858d482e9 Fill objectives when receiving new contract after claim 2025-03-25 20:05:29 +02:00
Yury 5e42a54bbf Add JobBoardsData document for job board data per player 2025-03-25 18:54:15 +02:00
Yury 7766d47bda Add EnterMatchmaking function for CB 2025-03-25 18:53:52 +02:00
Yury 31dbcfdb4f Change durability on inventory update 2025-03-25 18:53:09 +02:00
Yury 43e2b69d66 Set objective counter before deployment 2025-03-25 18:52:55 +02:00
Yury 7a0e0d1285 Fix possible exception on non-existent quest activation 2025-03-25 18:52:28 +02:00
Yury 36d797b391 Fix item sell formula for Ammo and Ability items 2025-03-25 18:51:55 +02:00
Yury 4e0b6440d9 Fix job boards 2025-03-25 18:51:29 +02:00
Yury eec90dae91 Set max objective progress for kills and visit quests 2025-03-25 18:50:38 +02:00
Yury 4e1c256fbe Update Prospect.Client.Loader 2025-03-25 18:49:27 +02:00
Yury 7a370fded1 Update features 2025-03-05 00:14:47 +02:00
Yury ff54118493 Add note regarding Loader compatibility with game versions 2025-03-05 00:10:45 +02:00
Yury 840efb8383 Add Prospect.Client.Loader and update Solution config 2025-03-04 23:56:50 +02:00
Yury c3dbfa5d59 Update instructions 2025-03-04 23:56:37 +02:00
Yury dfc38de7b5 Push API changes
* Fix item repair cost formula
* Add "RequestUpdateAvailableFreeLoadout" function (WIP)
* Fix item sell formula
* Fix item stack split on sell
* Fix contract progression counting for objectives other than owned items
* Refactor balance into a proper struct
* Separate and update Season 2 and Season 3 data
2025-03-04 23:39:20 +02:00
YuryandGitHub 08ca12f1e3 Update README.md 2025-02-24 11:03:32 +02:00
YuryandGitHub fb9acbbedc Update README.md 2025-02-24 11:01:37 +02:00
Yury 280b87314c Push changes 2025-02-24 01:06:32 +02:00
Yury 1bfb16539f Fix error message to match console log 2025-02-13 21:00:08 +02:00
Yury 7d0b7546b0 Update features 2025-02-13 20:57:48 +02:00
Yury 07d36f3e8b Update specific steps and troubleshooting section 2025-02-13 20:56:10 +02:00
144 changed files with 8387 additions and 854 deletions
+33
View File
@@ -0,0 +1,33 @@
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
@@ -0,0 +1 @@
_out/
+15
View File
@@ -0,0 +1,15 @@
# 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
@@ -0,0 +1,80 @@
# 🎮 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.*
+136 -76
View File
@@ -1,47 +1,70 @@
# Prospect
# Prospect <!-- omit in toc -->
Also known as "The Cycle: Frontier".
## Table of Contents <!-- omit in toc -->
- [Features](#features)
- [Running locally](#running-locally)
- [1. Prerequisites](#1-prerequisites)
- [1.1 How to download Season 2 client from SteamDB using Steam console](#11-how-to-download-season-2-client-from-steamdb-using-steam-console)
- [2. Unpack `Prospect.Server.Api`](#2-unpack-prospectserverapi)
- [3. 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)
- [Troubleshooting and FAQ](#troubleshooting-and-faq)
- [How to remove the certificate?](#how-to-remove-the-certificate)
- [`generate_ssl.exe` is flagged as a virus](#generate_sslexe-is-flagged-as-a-virus)
- [Body parts are missing with Season 3 client](#body-parts-are-missing-with-season-3-client)
- [Prospect.Server.Api does not start](#prospectserverapi-does-not-start)
- [Login Failed. Error code: 3](#login-failed-error-code-3)
- [Login Failed. Error code: 5](#login-failed-error-code-5)
- [Development](#development)
## Features
* [x] Basic login with Steam.
* [x] EULA acceptance.
* [x] Tutorial.
* [x] Single-player station (Season 2 and Season 3).
* Basic station functionality:
* [ ] Onboarding
* [x] Talk to Badum
* [x] Equip items
* [ ] Deploy to Fortuna
* [ ] Matchmaking
* [ ] Can deploy through matchmaking
* [x] Basic login with Steam
* [x] EULA acceptance
* [x] Tutorial
* [x] Single-player station (Season 2 and Season 3):
* [x] Onboarding
* [ ] Matchmaking and deployment
* [x] Solo
* [ ] Squad
* [ ] Items insurance
* [ ] Free loadouts (Season 3)
* [ ] Inventory and loadout
* [x] Items can be equipped
* [x] Items can be unequipped.
* [x] Loadout is carried into matchmaking
* [ ] Items are automatically stacked
* [ ] Quests
* [x] Quests can be accepted
* [ ] Quests can be removed
* [ ] Quest progress is tracked
* [ ] Quests can be completed
* [ ] Job boards
* [ ] Faction progression
* [x] Inventory and loadout
* [ ] Loadout presets (Season 3)
* [x] Quests
* [x] Faction progression
* [ ] Season pass
* [ ] Aurum Shops
* [ ] Daily shop
* [ ] Weekly shop
* [ ] Shop rotation
* [x] Daily login
* [x] Character appearance and emotes
* [ ] Shops
* [x] Items can be bought
* [ ] Items can be sold
* [ ] Season pass
* [ ] Quarters
* [ ] Generator
* [ ] Quarter upgrades
* [ ] Player balance
* [x] K-Marks
* [x] Aurum
* [ ] Insurance Tokens
* [x] Item Shops
* [x] Crafting station
* [x] Quarters
* [x] Player balance
* [ ] Social features
* [ ] Proximity voice
* [x] Vivox login
* [x] Vivox create and join channel
* [ ] Proximity voice works
* [x] 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
* [x] Bright Sands
* [x] Crescent Falls
* [x] Tharis Island
## Running locally
@@ -50,37 +73,53 @@ Also known as "The Cycle: Frontier".
### 1. Prerequisites
> [!WARNING]
> The latest Steam version of The Cycle: Frontier currently does not work with Windows 11 24H2!
> [!IMPORTANT]
> You must have The Cycle: Frontier from Steam in your Steam library to be able to download it.
> Otherwise, the download will fail with an error message about missing license.
Before you start, you'll need the following software downloaded and installed:
1. [MongoDB Community Edition](https://fastdl.mongodb.org/windows/mongodb-windows-x86_64-8.0.4-signed.msi).
1. [Latest `Prospect.Server.Api`](https://github.com/deiteris/Prospect/releases) from the Releases section
1. [`Prospect.Server.Api` and `LoaderPack`](https://github.com/deiteris/Prospect/releases) from the Releases section:
1. [.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 3 (the latest Steam game client), use Build 6.
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.
- For Season 2 game client, use the latest version.
1. A compatible The Cycle: Frontier game client. The server works with:
1. The Cycle: Frontier game client:
1. The latest version from [Steam](https://steamcommunity.com/app/868270).
- The latest version from [Steam](https://steamcommunity.com/app/868270).
1. Season 2 client downloaded from SteamDB using a depot downloader. For example, [version 2.7.2](https://steamdb.info/depot/868271/history/?changeid=M:4623363103423775682).
- Season 2 client version `4623363103423775682` from SteamDB. 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
> [!WARNING]
> This will overwrite the existing client if you try to download a different manifest!
1. With Steam running, press `Win+R` and enter `steam://nav/console`. A Steam console will open.
1. Open [The Cycle: Frontier SteamDB manifests](https://steamdb.info/depot/868271/manifests/).
1. Make sure you have **Copy format** set to **Steam console**.
1. Press `CTRL+F` and enter `4623363103423775682` to find the manifest for Season 2 version 2.7.2 client.
1. Click the ![Copy](./_assets/steamdb_copy.PNG) icon to copy the download command.
1. Paste the command in the Steam console and press `Enter`.
1. The depot will begin downloading. You should receive a notification and the destination folder when the download is complete.
### 2. Unpack `Prospect.Server.Api`
Use your favorite ZIP archiver and unzip the `Prospect.Server.Api.zip` downloaded from this repository.
### 3. Patch hosts
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.
### 4. Generate and import SSL certificate
### 3. 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.
@@ -102,41 +141,32 @@ to successfully communicate with the local server. Do the following:
1. Select **Place all certificates in the following store** > **Browse...**. Choose **Trusted Root Certification Authorities** and click **OK**. Click **Next**.
1. Click **Finish**. A Security Warning popup may appear, make sure it specifies `2EA46.playfabapi.com` certification authority and click **Yes**.
1. Click **Finish**. A **Security Warning** popup may appear, make sure it specifies `2EA46.playfabapi.com` certification authority and click **Yes**.
### 5. Add `steam_appid` to the game
### 4. Extract `LoaderPack` to the game
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
1. Right-click in the folder > **New** > **Text Document**, and name it `steam_appid`.
1. Open the `LoaderPack` archive.
1. Open `steam_appid` and enter `480`.
1. Drag and drop the contents of the `LoaderPack` archive to the game.
1. Save and close the file.
1. Create a shortcut for the `Prospect.Client.Loader` that you will use later to launch the game.
### 6. Create a game shortcut with specified arguments
### 5. Run the server
1. Open the folder with The Cycle: Frontier and navigate to **Prospect** > **Binaries** > **Win64**.
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.
1. Right-click `Prospect-Win64-Shipping.exe` > **Create Shortcut**.
> [!IMPORTANT]
> Do not close the console when you run the game.
1. Right-click the created shortcut > **Properties**.
1. In the **Target** field, add the following parameters after the quote ` -empty -server -log -windowed -noeac -nointro -steam_auth PF_TITLEID=2EA46`. Note the space after the quote.
1. Move the shortcut to desktop.
### 7. Run the server
Now you are all set! Open the folder with `Prospect.Server.Api` and run `Prospect.Server.Api.exe`.
### 8. Run the game
### 6. 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.
### Troubleshooting and FAQ
## Troubleshooting and FAQ
#### How to remove the certificate?
### How to remove the certificate?
If you've installed the certificate for the **Current User**:
@@ -148,15 +178,45 @@ If you've installed the certificate for the **Current User**:
If you've installed the certificate for the **Local Machine**, repeat the same steps but instead open `certlm.msc`.
#### `generate_ssl.exe` is flagged as a virus
### `generate_ssl.exe` is flagged as a virus
`generate_ssl.exe` is a Python application packed with PyInstaller and some anti-viruses may flag it as a virus.
This application is a simple certificate generator and you can find its source code in `utils/generate_ssl.py`.
#### Body parts are missing with Season 3 client
### Body parts are missing with Season 3 client
Currently, the server loads body part IDs for Season 2 by default, so this is expected. You can fix this by going to station and changing your character appearance. This will store the updated body part IDs for your character.
### Prospect.Server.Api does not start
Make sure you have [.NET Runtime 8.0](https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.11) and [ASP.NET Core 8.0](https://aka.ms/dotnet-core-applaunch?framework=Microsoft.AspNetCore.App&framework_version=8.0.0&arch=x64&rid=win-x64&os=win10) installed.
### Login Failed. Error code: 3
Make sure that:
* You have Steam running.
* You have created and **saved** the `steam_appid` file as described in step 6.
* The `steam_appid` file type is "TXT File".
### Login Failed. Error code: 5
Make sure that `Prospect.Server.Api` server is running.
If the server is running, 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`.
![](./_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.
![](./_assets/certificate_error.PNG)
* `HTTP code: 500` - usually indicates that MongoDB is not running. Make sure that MongoDB is installed and and that `MongoDB Server` is running in `services.msc`.
![](./_assets/mongodb_error.PNG)
## Development
TBD
TBD
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 B

+2 -2
View File
@@ -45,13 +45,13 @@
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<LinkIncremental>true</LinkIncremental>
<OutDir>$(ProjectDir)..\Prospect.Launcher\</OutDir>
<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)..\Prospect.Launcher\</OutDir>
<OutDir>$(ProjectDir)bin\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir)bin\intermediate\$(Platform)\$(Configuration)\</IntDir>
<LibraryPath>$(LibraryPath)</LibraryPath>
</PropertyGroup>
+62 -28
View File
@@ -5,19 +5,40 @@
#include "logger.h"
#include "SDK.h"
#include "SDK_Memory.h"
#include <string>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
#include <locale>
// Pointers are for 211025.1143
// https://steamdb.info/depot/1600361/history/?changeid=M:720055916602660127
std::wstring g_backendUrl;
/**
* Variable offset, find with "STOPMOVIECAPTURE", scroll up.
*/
constexpr uintptr_t OffsetGMalloc = 0x5C9D478;
// 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 = 0xCA5010;
constexpr uintptr_t OffsetPlayFabApiGetUrl = 0xCEEF00;
/**
* Function hooks.
@@ -26,15 +47,18 @@ typedef SDK::FString(__fastcall* tPlayFabApiGetUrl)(SDK::UPlayFabAPISettings*, c
tPlayFabApiGetUrl OldPlayFabApiGetUrl;
SDK::FString __fastcall PlayFabApiGetUrlProxy(SDK::UPlayFabAPISettings* thiz, const SDK::FString& callPath)
{
logger::Print("[Agent] GetURL called with callPath %s\n", callPath.ToString().c_str());
return std::wstring(L"http://127.0.0.1:5000") + std::wstring(callPath.c_str());
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)
{
DWORD WINAPI OnDllAttach(LPVOID base) {
logger::Print("[Agent] DLL Attached\n");
const auto hModule = GetModuleHandleW(nullptr);
@@ -44,8 +68,7 @@ DWORD WINAPI OnDllAttach(LPVOID base)
SDK::GMalloc = reinterpret_cast<SDK::FMalloc**>(hModulePtr + OffsetGMalloc);
// Initialize MinHook.
if (MH_Initialize() != MH_OK)
{
if (MH_Initialize() != MH_OK) {
logger::Print("[Agent] Failed to initialize MinHook\n");
FreeLibraryAndExitThread(hModule, 1);
}
@@ -55,18 +78,33 @@ DWORD WINAPI OnDllAttach(LPVOID base)
const auto ppOriginal = reinterpret_cast<LPVOID*>(&OldPlayFabApiGetUrl);
const auto mhStatus = MH_CreateHook(pTarget, &PlayFabApiGetUrlProxy, ppOriginal);
if (mhStatus != MH_OK)
{
if (mhStatus != MH_OK) {
logger::Print("[Agent] Failed to create PlayFabAPIGetUrl hook (%d)\n", mhStatus);
FreeLibraryAndExitThread(hModule, 1);
}
if (MH_EnableHook(pTarget) != MH_OK)
{
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);
}
@@ -75,10 +113,8 @@ BOOL WINAPI DllMain(
const _In_ HINSTANCE hinstDll,
const _In_ DWORD fdwReason,
const _In_opt_ LPVOID lpvReserved
)
{
if (fdwReason == DLL_PROCESS_ATTACH)
{
) {
if (fdwReason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hinstDll);
// Allocate a console.
@@ -87,12 +123,10 @@ BOOL WINAPI DllMain(
// Attach logger.
logger::Attach();
// Spawn thread.
CreateThread(nullptr, 0, OnDllAttach, hinstDll, 0, nullptr);
}
else if (fdwReason == DLL_PROCESS_DETACH)
{
} else if (fdwReason == DLL_PROCESS_DETACH) {
// Detach logger.
logger::Detach();
}
@@ -0,0 +1,138 @@
<?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>
@@ -0,0 +1,22 @@
<?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
@@ -0,0 +1,500 @@
#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;
}
-55
View File
@@ -1,55 +0,0 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
using Prospect.Launcher.Invoke;
namespace Prospect.Launcher;
public static class Inject
{
const int PROCESS_CREATE_THREAD = 0x0002;
const int PROCESS_QUERY_INFORMATION = 0x0400;
const int PROCESS_VM_OPERATION = 0x0008;
const int PROCESS_VM_WRITE = 0x0020;
const int PROCESS_VM_READ = 0x0010;
const uint MEM_COMMIT = 0x00001000;
const uint MEM_RESERVE = 0x00002000;
const uint PAGE_READWRITE = 4;
// https://codingvision.net/c-inject-a-dll-into-a-process-w-createremotethread
public static unsafe bool Library(uint processId, string path)
{
IntPtr procHandle = Kernel32.OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, false, (int)processId);
var loadLibraryAddr = Kernel32.GetProcAddress(Kernel32.GetModuleHandle("kernel32.dll"), "LoadLibraryA");
if (loadLibraryAddr == IntPtr.Zero)
{
Console.WriteLine("Failed GetProcAddress.");
return false;
}
var allocMemAddress = Kernel32.VirtualAllocEx(procHandle, IntPtr.Zero, (uint)((path.Length + 1) * Marshal.SizeOf(typeof(char))), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (allocMemAddress == IntPtr.Zero)
{
Console.WriteLine("Failed VirtualAllocEx.");
return false;
}
var libraryBytes = Encoding.Default.GetBytes(path);
fixed (byte* pLibraryBytes = libraryBytes)
{
Kernel32.WriteProcessMemory(procHandle, allocMemAddress, pLibraryBytes, libraryBytes.Length + 1, out _);
}
var threadHandle = Kernel32.CreateRemoteThread(procHandle, IntPtr.Zero, 0, loadLibraryAddr, allocMemAddress, 0, IntPtr.Zero);
if (threadHandle == IntPtr.Zero)
{
Console.WriteLine("Failed CreateRemoteThread.");
return false;
}
return true;
}
}
-52
View File
@@ -1,52 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Prospect.Launcher.Invoke.Structs;
namespace Prospect.Launcher.Invoke;
internal static class Kernel32
{
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CreateProcess(string lpApplicationName,
string lpCommandLine, IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles, ProcessCreationFlags dwCreationFlags,
IntPtr lpEnvironment, string lpCurrentDirectory,
ref StartupInfo lpStartupInfo,
out ProcessInformation lpProcessInformation);
public static bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
ProcessCreationFlags dwCreationFlags,
ref StartupInfo lpStartupInfo,
out ProcessInformation lpProcessInformation) => CreateProcess(lpApplicationName, lpCommandLine, IntPtr.Zero,
IntPtr.Zero, false, dwCreationFlags, IntPtr.Zero, null, ref lpStartupInfo, out lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern unsafe bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte* lpBuffer,
Int32 nSize,
out IntPtr lpNumberOfBytesWritten
);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);
[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr GetProcAddress(IntPtr hModule, string procName);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint ResumeThread(IntPtr hThread);
}
@@ -1,25 +0,0 @@
using System;
namespace Prospect.Launcher.Invoke.Structs;
[Flags]
internal enum ProcessCreationFlags : uint
{
ZERO_FLAG = 0x00000000,
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00001000,
CREATE_SHARED_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
INHERIT_PARENT_AFFINITY = 0x00010000
}
@@ -1,13 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Prospect.Launcher.Invoke.Structs;
[StructLayout(LayoutKind.Sequential)]
internal struct ProcessInformation
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
@@ -1,27 +0,0 @@
using System;
using System.Runtime.InteropServices;
namespace Prospect.Launcher.Invoke.Structs;
[StructLayout(LayoutKind.Sequential)]
internal struct StartupInfo
{
public uint cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public uint dwX;
public uint dwY;
public uint dwXSize;
public uint dwYSize;
public uint dwXCountChars;
public uint dwYCountChars;
public uint dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
-77
View File
@@ -1,77 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using Prospect.Launcher.Invoke;
using Prospect.Launcher.Invoke.Structs;
namespace Prospect.Launcher;
internal static class Program
{
/// <summary>
/// Steam AppId.
/// - https://steamdb.info/app/480/
/// - https://steamdb.info/app/1600360/
/// </summary>
private const string AppId = "480";
/// <summary>
/// Executable name of the game.
/// </summary>
private const string FileName = "Prospect-Win64-Shipping.exe";
private static void Main(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("Usage: ./Prospect.Launcher <Game directory>");
return;
}
var gamePath = Path.Combine(args[0], "Prospect", "Binaries", "Win64");
var gameBinary = Path.Combine(gamePath, FileName);
var gameArgs = new List<string>();
// 2EA46 = Default
// A22AB = The Cycle Playtest
gameArgs.Add("-empty");
gameArgs.Add("-log");
gameArgs.Add("-windowed");
gameArgs.Add("-noeac");
gameArgs.Add("-nointro");
gameArgs.Add("-steam_auth");
gameArgs.Add("PF_TITLEID=A22AB");
// Ensure "steam_appid.txt" exists, to fix steam authentication.
var steamAppId = Path.Combine(gamePath, "steam_appid.txt");
File.WriteAllText(steamAppId, AppId);
// Spawn.
var startupInfo = new StartupInfo();
if (!Kernel32.CreateProcess(gameBinary, string.Join(' ', gameArgs), ProcessCreationFlags.CREATE_SUSPENDED, ref startupInfo, out var processInfo))
{
Console.WriteLine("Failed to spawn game.");
return;
}
Console.WriteLine("Spawned.");
// Inject DLL.
var agentPath = Path.Combine(AppContext.BaseDirectory, "Prospect.Agent.dll");
if (!Inject.Library(processInfo.dwProcessId, agentPath))
{
Console.WriteLine("Failed to inject library.");
return;
}
Console.WriteLine("Injected.");
// Resume.
Kernel32.ResumeThread(processInfo.hThread);
Console.WriteLine("Resumed.");
}
}
@@ -1,30 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>false</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishReadyToRun>true</PublishReadyToRun>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<Content Include="Prospect.Agent.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
</Project>
@@ -4,4 +4,6 @@ public class PlayFabSettings
{
public string PublisherId { get; set; }
public string TitleId { get; set; }
public string SignalRURL { get; set; }
public string SignalRAccessToken { get; set; }
}
@@ -1,10 +1,12 @@
using System.Net.Mime;
using System.Text.Json;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
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;
@@ -21,6 +23,7 @@ public class ClientController : Controller
{
private const int AppIdDefault = 480;
private const int AppIdCycleBeta = 1600361;
private const int AppIdCycle = 868270;
private readonly ILogger<ClientController> _logger;
private readonly PlayFabSettings _settings;
@@ -31,9 +34,9 @@ public class ClientController : Controller
private readonly TitleDataService _titleDataService;
public ClientController(ILogger<ClientController> logger,
IOptions<PlayFabSettings> settings,
AuthTokenService authTokenService,
DbUserService userService,
IOptions<PlayFabSettings> settings,
AuthTokenService authTokenService,
DbUserService userService,
DbEntityService entityService,
UserDataService userDataService,
TitleDataService titleDataService)
@@ -46,7 +49,7 @@ public class ClientController : Controller
_userDataService = userDataService;
_titleDataService = titleDataService;
}
[AllowAnonymous]
[HttpPost("LoginWithSteam")]
[Produces(MediaTypeNames.Application.Json)]
@@ -55,18 +58,18 @@ public class ClientController : Controller
if (!string.IsNullOrEmpty(request.SteamTicket))
{
var ticket = AppTicket.Parse(request.SteamTicket);
if (ticket.IsValid && ticket.HasValidSignature && (ticket.AppId == AppIdDefault || ticket.AppId == AppIdCycleBeta))
if (ticket.IsValid && ticket.HasValidSignature && (ticket.AppId == AppIdDefault || ticket.AppId == AppIdCycleBeta || ticket.AppId == AppIdCycle))
{
var userSteamId = ticket.SteamId.ToString();
var user = await _userService.FindOrCreateAsync(PlayFabUserAuthType.Steam, userSteamId);
var entity = await _entityService.FindOrCreateAsync(user.Id);
var userTicket = _authTokenService.GenerateUser(entity);
var entityTicket = _authTokenService.GenerateEntity(entity);
await _userDataService.InitAsync(user.Id);
return Ok(new ClientResponse<FServerLoginResult>
{
Code = 200,
@@ -116,14 +119,14 @@ public class ClientController : Controller
}
});
}
_logger.LogWarning("Invalid steam ticket specified, IsExpired {IsExpired}, IsValid {IsValid}, HasValidSignature {Sig}, AppId {AppId}",
ticket.IsExpired,
ticket.IsValid,
ticket.HasValidSignature,
ticket.AppId);
}
return BadRequest(new ClientResponse
{
Code = 400,
@@ -147,6 +150,127 @@ public class ClientController : Controller
});
}
[HttpPost("GetStoreItems")]
[Produces(MediaTypeNames.Application.Json)]
[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>
{
Code = 200,
Status = "OK",
Data = new FGetStoreItemsResult
{
StoreId = string.IsNullOrEmpty(storeId) ? null : storeId,
CatalogVersion = "StaticItems",
Store = store,
}
});
}
[HttpPost("UpdateUserTitleDisplayName")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
@@ -170,7 +294,7 @@ public class ClientController : Controller
{
var userId = User.FindAuthUserId();
await _userDataService.UpdateAsync(userId, request.PlayFabId, request.Data);
return Ok(new ClientResponse<FUpdateUserDataResult>
{
Code = 200,
@@ -189,7 +313,7 @@ public class ClientController : Controller
{
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, request.PlayFabId, request.Keys);
return Ok(new ClientResponse<FGetUserDataResult>
{
Code = 200,
@@ -205,15 +329,18 @@ public class ClientController : Controller
[HttpPost("GetUserReadOnlyData")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetUserReadOnlyData(FGetUserDataRequest request)
public async Task<IActionResult> GetUserReadOnlyData(FGetUserDataRequest request)
{
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, request.PlayFabId, request.Keys);
return Ok(new ClientResponse<FGetUserDataResult>
{
Code = 200,
Status = "OK",
Data = new FGetUserDataResult
{
Data = new Dictionary<string, FUserDataRecord>(),
Data = userData,
DataVersion = 0
}
});
@@ -222,45 +349,124 @@ public class ClientController : Controller
[HttpPost("GetUserInventory")]
[Produces(MediaTypeNames.Application.Json)]
[Authorize(AuthenticationSchemes = UserAuthenticationOptions.DefaultScheme)]
public IActionResult GetUserReadOnlyData(FGetUserInventoryRequest request)
public async Task<IActionResult> GetUserInventory(FGetUserInventoryRequest request)
{
var userId = User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Balance", "Inventory", "VanityItems", "CharacterVanity", "GlobalVanity"});
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"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
List<FItemInstance> items = new List<FItemInstance>();
foreach (var item in inventory) {
if (!blueprints.ContainsKey(item.BaseItemId)) {
_logger.LogWarning("Failed to find item with ID {ItemID}", item.BaseItemId);
continue;
}
var blueprintData = blueprints[item.BaseItemId];
items.Add(new FItemInstance {
ItemId = item.BaseItemId,
ItemInstanceId = item.ItemId,
ItemClass = blueprintData.Kind,
// PurchaseDate = DateTime.Now, // TODO
// UnitPrice = 0,
CatalogVersion = "StaticItems",
CustomData = new Dictionary<string, string> {
["mods"] = JsonSerializer.Serialize(item.ModData),
// ["insurance"] = "None", // TODO
["vanity"] = $"{{\"p\":{item.PrimaryVanityId},\"s\":{item.SecondaryVanityId}}}", // p - primary, s - secondary
["coreData"] = $"{{\"a\":{item.Amount},\"d\":{item.Durability}}}", // a - amount, d - durability
["origin"] = JsonSerializer.Serialize(item.Origin),
["rolledPerks"] = JsonSerializer.Serialize(item.RolledPerks),
}
});
}
foreach (var vanityId in ownedVanityIds) {
items.Add(new FItemInstance {
ItemId = vanityId,
// ItemInstanceId is not required since it's not a unique item.
ItemClass = "Vanity",
// PurchaseDate = DateTime.Now, // TODO
CatalogVersion = "StaticItems",
CustomData = new Dictionary<string, string> {
["coreData"] = "{}", // Vanity items don't seem to have any coreData. But it's still required since it's an inventory item.
["origin"] = "{}",
}
});
}
return Ok(new ClientResponse<FGetUserInventoryResult>
{
Code = 200,
Status = "OK",
Data = new FGetUserInventoryResult
{
Inventory = new List<FItemInstance>
{
// new FItemInstance
// {
// ItemId = "Helmet_03",
// ItemInstanceId = "0102030405060708",
// ItemClass = "Helmet",
// PurchaseDate = DateTime.Now.AddDays(-1),
// CatalogVersion = "StaticItems",
// DisplayName = "Helmet",
// UnitPrice = 0,
// CustomData = new Dictionary<string, string>
// {
// ["insurance"] = "None",
// ["mods"] = "{\"m\":[]}",
// ["vanity_misc_data"] = "{\"v\":\"\",\"s\":\"\",\"l\":3,\"a\":1,\"d\":300}"
// }
// }
},
VirtualCurrency = new Dictionary<string, int>
{
["AE"] = 0,
["AS"] = 0,
["AU"] = int.MaxValue,
["SC"] = int.MaxValue
},
Inventory = items,
VirtualCurrency = JsonSerializer.Deserialize<PlayerBalance>(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)]
@@ -29,7 +29,7 @@ public class CloudScriptController : Controller
var result = await _cloudScriptService.ExecuteAsync(request.FunctionName, request.FunctionParameter, request.GeneratePlayStreamEvent);
if (result != null)
{
{
return Ok(new ClientResponse<FExecuteFunctionResult>
{
Code = 200,
+5 -1
View File
@@ -4,5 +4,9 @@ namespace Prospect.Server.Api.Hubs;
public class CycleHub : Hub
{
public override async Task OnConnectedAsync()
{
Console.WriteLine("Connected {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
}
@@ -22,15 +22,15 @@ public class RequestLoggerMiddleware
if (context.Request.Method == "POST")
{
var requestId = "NULL";
if (context.Request.Headers.TryGetValue("X-RequestID", out var requestIdValues))
{
requestId = requestIdValues.ToString();
}
_logger.LogDebug("URL {Url} RequestId {RequestId} Body {Body}", context.Request.GetDisplayUrl(), requestId, body);
}
await _next(context);
}
@@ -0,0 +1,39 @@
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();
}
@@ -9,7 +9,7 @@ public class FUserDataRecord
/// </summary>
[JsonPropertyName("LastUpdated")]
public DateTime LastUpdated { get; set; }
/// <summary>
/// [optional] Indicates whether this data can be read by all users (public) or only the user (private). This is used for GetUserData
/// requests being made by one player about another player.
@@ -17,7 +17,7 @@ public class FUserDataRecord
[JsonPropertyName("Permission")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public UserDataPermission? Permission { get; set; }
/// <summary>
/// [optional] Data stored for the specified user data key.
/// </summary>
@@ -0,0 +1,19 @@
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; }
}
@@ -0,0 +1,25 @@
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; }
}
@@ -10,7 +10,7 @@ public class FGetUserDataResult
/// </summary>
[JsonPropertyName("Data")]
public Dictionary<string, FUserDataRecord>? Data { get; set; }
/// <summary>
/// [optional] Indicates the current version of the data that has been set. This is incremented with every set call for that type of
/// data (read-only, internal, etc). This version can be provided in Get calls to find updated data.
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Client.Data;
using Prospect.Server.Api.Models.Data;
namespace Prospect.Server.Api.Models.Client;
@@ -11,14 +12,14 @@ public class FGetUserInventoryResult
[JsonPropertyName("Inventory")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public List<FItemInstance>? Inventory { get; set; }
/// <summary>
/// [optional] Array of virtual currency balance(s) belonging to the user.
/// </summary>
[JsonPropertyName("VirtualCurrency")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Dictionary<string, int>? VirtualCurrency { get; set; }
public PlayerBalance? VirtualCurrency { get; set; }
/// <summary>
/// [optional] Array of virtual currency balance(s) belonging to the user.
/// </summary>
@@ -0,0 +1,25 @@
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; }
}
@@ -0,0 +1,139 @@
using System.Text.Json.Serialization;
public class TitleDataContractInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Faction")]
public string Faction { get; set; }
[JsonPropertyName("ReputationIncrease")]
public int ReputationIncrease { get; set; }
[JsonPropertyName("Rewards")]
public TitleDataContractInfoReward[] Rewards { get; set; }
[JsonPropertyName("Objectives")]
public TitleDataContractInfoObjective[] Objectives { get; set; }
[JsonPropertyName("UnlockData")]
public TitleDataContractInfoUnlockData UnlockData { get; set; }
[JsonPropertyName("IsMainContract")]
public bool IsMainContract { get; set; }
[JsonPropertyName("ContractDifficulty")]
public EYContractDifficulty ContractDifficulty { get; set; }
}
public class TitleDataContractInfoReward {
[JsonPropertyName("ItemID")]
public string ItemID { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
public class TitleDataContractInfoObjective {
[JsonPropertyName("Type")]
public EYContractObjectiveType Type { get; set; }
[JsonPropertyName("MaxProgress")]
public int MaxProgress { get; set; }
[JsonPropertyName("ItemToOwn")]
public string ItemToOwn { get; set; }
[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 enum EYContractObjectiveType {
Invalid = 0,
Kills = 1,
OwnNumOfItem = 2,
DeadDrop = 3,
VisitArea = 4,
LootContainer = 5,
FactionLevel = 6,
CompletedMission = 7,
MAX = 8
}
public enum EYContractDifficulty
{
Invalid = 0,
Easy = 1,
Medium = 2,
Hard = 3,
MAX = 4
};
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
};
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
public class TitleDataDailyCrateInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("Weight")]
public float Weight { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
@@ -0,0 +1,142 @@
using System.Text.Json.Serialization;
public class TitleDataBlueprintInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("ItemShopsCraftingData")]
public Dictionary<string, TitleDataItemShopsCraftingData> ItemShopsCraftingData { get; set; }
[JsonPropertyName("AmountPerPurchase")]
public int AmountPerPurchase { get; set; }
[JsonPropertyName("DurabilityMax")]
public int DurabilityMax { get; set; }
[JsonPropertyName("DurabilityBrokenScrappingReturnModifier")]
public double DurabilityBrokenScrappingReturnModifier { get; set; }
[JsonPropertyName("RepairCostMaxDurability")]
public int RepairCostMaxDurability { get; set; }
[JsonPropertyName("RepairCostBase")]
public int RepairCostBase { get; set; }
[JsonPropertyName("RepairCostModifierBroken")]
public int RepairCostModifierBroken { get; set; }
[JsonPropertyName("ItemWeight")]
public double ItemWeight { get; set; }
[JsonPropertyName("ScrappingReturnDefault")]
public int ScrappingReturnDefault { get; set; }
[JsonPropertyName("OverrideScrappingReturns")]
public int OverrideScrappingReturns { get; set; }
[JsonPropertyName("OverrideScrappingReputation")]
public int OverrideScrappingReputation { get; set; }
[JsonPropertyName("ScrappingFactionProgressionIncrement")]
public int ScrappingFactionProgressionIncrement { get; set; }
[JsonPropertyName("UnlockData")]
public TitleDataItemUnlockData UnlockData { get; set; }
[JsonPropertyName("ModID")]
public int? ModID { get; set; }
[JsonPropertyName("AlienForgeCatalystToUpgradedItemMap")]
public TitleDataItemAlienForgeCatalystToUpgradedItemMap AlienForgeCatalystToUpgradedItemMap { get; set; }
[JsonPropertyName("Rarity")]
public int Rarity { get; set; }
[JsonPropertyName("Kind")]
public string Kind { get; set; }
[JsonPropertyName("MaxAmountPerStack")]
public int MaxAmountPerStack { get; set; }
}
public class TitleDataItemShopsCraftingData {
[JsonPropertyName("ItemRecipeIngredients")]
public List<TitleDataItemRecipeCostType> ItemRecipeIngredients { get; set; }
[JsonPropertyName("UpgradeTimeMinutes")]
public int UpgradeTimeMinutes { get; set; }
[JsonPropertyName("UpgradeTimeSeconds")]
public int UpgradeTimeSeconds { get; set; }
[JsonPropertyName("SkipCraftingMaxCost")]
public int SkipCraftingMaxCost { get; set; }
[JsonPropertyName("SkipOptionalCraftingMaxCost")]
public int SkipOptionalCraftingMaxCost { get; set; }
}
public class TitleDataItemRecipeCostType {
[JsonPropertyName("Item")]
public string Currency { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
public class TitleDataItemUnlockData {
[JsonPropertyName("FactionUnlockLevel")]
public int FactionUnlockLevel { get; set; }
[JsonPropertyName("ContractLockPurchase")]
public string ContractLockPurchase { get; set; }
[JsonPropertyName("ContractLockCrafting")]
public string ContractLockCrafting { get; set; }
}
public class TitleDataItemAlienForgeCatalystToUpgradedItemMap {
[JsonPropertyName("Catalyst")]
public string Catalyst { get; set; }
[JsonPropertyName("NextItem")]
public string NextItem { get; set; }
}
/*
enum class EYItemType : uint8_t
{
EYItemType__None = 0,
EYItemType__Device = 1,
EYItemType__Ability = 2,
EYItemType__Kit = 3,
EYItemType__Consumable = 4,
EYItemType__Mod = 5,
EYItemType__Blueprint = 6,
EYItemType__Material = 7,
EYItemType__Miscellaneous = 8,
EYItemType__Currency = 9,
EYItemType__Vanity = 10,
EYItemType__Experience = 11,
EYItemType__Lore = 12,
EYItemType__Vehicle = 13,
EYItemType__Ammo = 14,
EYItemType__Upgrade = 15,
EYItemType__Collectible = 16,
EYItemType__QuestItem = 17,
EYItemType__Shield = 18,
EYItemType__ProspectorBadge = 19,
EYItemType__TechTreeNode = 20,
EYItemType__PlayerQuartersLevel = 21,
EYItemType__PassiveGenerator = 22,
EYItemType__Bag = 23,
EYItemType__Helmet = 24,
EYItemType__Key = 25,
EYItemType__MeleeWeapon = 26,
EYItemType__All = 27,
EYItemType__MAX = 28
};
enum class EYItemOriginType : uint8_t
{
EYItemOriginType__Undefined = 0,
EYItemOriginType__Contract = 1,
EYItemOriginType__Craft = 2,
EYItemOriginType__Creature = 3,
EYItemOriginType__Debug = 4,
EYItemOriginType__FortunaPass = 5,
EYItemOriginType__FTUE = 6,
EYItemOriginType__Generator = 7,
EYItemOriginType__Insurance = 8,
EYItemOriginType__MapContainerLoot = 9,
EYItemOriginType__MapPickupLoot = 10,
EYItemOriginType__MatchSplitStack = 11,
EYItemOriginType__MatchUnknown = 12,
EYItemOriginType__PlayFabDashboard = 13,
EYItemOriginType__PurchaseICA = 14,
EYItemOriginType__PurchaseKorolev = 15,
EYItemOriginType__PurchaseOsiris = 16,
EYItemOriginType__PurchaseQuickShop = 17,
EYItemOriginType__Retention = 18,
EYItemOriginType__StationSplitStack = 19,
EYItemOriginType__TocMigration = 20,
EYItemOriginType__TwitchDrop = 21,
EYItemOriginType__Uplink = 22,
EYItemOriginType__WeaponAttachment = 23,
EYItemOriginType__EYItemOriginType_MAX = 24
};
*/
@@ -0,0 +1,46 @@
using System.Text.Json.Serialization;
public class TitleDataMissionInfo {
[JsonPropertyName("MissionId")]
public string MissionId { get; set; }
[JsonPropertyName("NextMissionRowHandle")]
public UDataTable NextMissionRowHandle { get; set; }
[JsonPropertyName("OnboardingRewards")]
public UDataTable OnboardingRewards { get; set; }
}
// {\"TalkToBadumReward\":{\"MissionRewardId\":\"TalkToBadumReward\",\"RewardEntries\":[{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_SMG_Bullet_01\"},\"RewardAmount\":2},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_AR_Energy_01\"},\"RewardAmount\":1},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_Pistol_Bullet_01\"},\"RewardAmount\":5},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"WP_E_SGun_Bullet_01\"},\"RewardAmount\":2},{\"RewardType\":6,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/AmmoTypes_DT.AmmoTypes_DT\\u0027\",\"RowName\":\"Light\"},\"RewardAmount\":6},{\"RewardType\":6,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/AmmoTypes_DT.AmmoTypes_DT\\u0027\",\"RowName\":\"Medium\"},\"RewardAmount\":4},{\"RewardType\":6,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/AmmoTypes_DT.AmmoTypes_DT\\u0027\",\"RowName\":\"Shotgun\"},\"RewardAmount\":2},{\"RewardType\":8,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Abilities.PRO_Abilities\\u0027\",\"RowName\":\"ShockGrenade_02\"},\"RewardAmount\":2},{\"RewardType\":8,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Abilities.PRO_Abilities\\u0027\",\"RowName\":\"Consumable_Health_01\"},\"RewardAmount\":5},{\"RewardType\":0,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_PlayerShield.PRO_PlayerShield\\u0027\",\"RowName\":\"Shield_01\"},\"RewardAmount\":5},{\"RewardType\":10,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Bags.PRO_Bags\\u0027\",\"RowName\":\"Bag_01\"},\"RewardAmount\":5},{\"RewardType\":7,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/PRO_Weapons.PRO_Weapons\\u0027\",\"RowName\":\"TOOL_MineralScanner_01\"},\"RewardAmount\":2}]},\"FirstMatchReward\":{\"MissionRewardId\":\"FirstMatchReward\",\"RewardEntries\":[{\"RewardType\":2,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Materials_DT.Materials_DT\\u0027\",\"RowName\":\"Insulation\"},\"RewardAmount\":4},{\"RewardType\":1,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Currencies_DT.Currencies_DT\\u0027\",\"RowName\":\"SoftCurrency\"},\"RewardAmount\":1500},{\"RewardType\":2,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Materials_DT.Materials_DT\\u0027\",\"RowName\":\"GenericCreatureDrop\"},\"RewardAmount\":3},{\"RewardType\":2,\"RewardRowHandle\":{\"DataTable\":\"DataTable\\u0027/Game/DataTables/Materials_DT.Materials_DT\\u0027\",\"RowName\":\"ICAScrip\"},\"RewardAmount\":1}]}}",
public class TitleDataMissionRewardInfo {
[JsonPropertyName("MissionRewardId")]
public string MissionRewardId { get; set; }
[JsonPropertyName("RewardEntries")]
public TitleDataMissionRewardEntry[] RewardEntries { get; set; }
}
public class TitleDataMissionRewardEntry {
[JsonPropertyName("RewardType")]
public int RewardType { get; set; }
[JsonPropertyName("RewardRowHandle")]
public UDataTable RewardRowHandle { get; set; }
[JsonPropertyName("RewardAmount")]
public int RewardAmount { get; set; }
}
/*
enum class EYRewardType : uint8_t
{
EYRewardType__None = 0,
EYRewardType__Currency = 1,
EYRewardType__Material = 2,
EYRewardType__Reputation = 3,
EYRewardType__SeasonXP = 4,
EYRewardType__ProspectorLevel = 5,
EYRewardType__Ammo = 6,
EYRewardType__Weapon = 7,
EYRewardType__Consumable = 8,
EYRewardType__Armor = 9,
EYRewardType__Bag = 10,
EYRewardType__MAX = 11
};
*/
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
public class TitleDataPassiveGeneratorsInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("GeneratorID")]
public string GeneratorID { get; set; }
[JsonPropertyName("BaseGenIntervalMinutes")]
public int BaseGenIntervalMinutes { get; set; }
[JsonPropertyName("BaseGenRate")]
public int BaseGenRate { get; set; }
[JsonPropertyName("BaseCap")]
public int BaseCap { get; set; }
}
@@ -0,0 +1,23 @@
using System.Text.Json.Serialization;
public class TitleDataPlayerQuartersInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("NodeUpgradesRequired")]
public int NodeUpgradesRequired { get; set; }
[JsonPropertyName("UpgradeCosts")]
public TitleDataPlayerQuartersUpgradeCosts[] UpgradeCosts { get; set; }
[JsonPropertyName("UpgradeSeconds")]
public int UpgradeSeconds { get; set; }
[JsonPropertyName("InitialRushCosts")]
public int InitialRushCosts { get; set; }
[JsonPropertyName("OptionalRushCosts")]
public int OptionalRushCosts { get; set; }
}
public class TitleDataPlayerQuartersUpgradeCosts {
[JsonPropertyName("Currency")]
public string Currency { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
@@ -0,0 +1,8 @@
using System.Text.Json.Serialization;
public class TitleDataRetentionBonusInfo {
[JsonPropertyName("Rewards")]
public string[] Rewards { get; set; }
[JsonPropertyName("Active")]
public bool Active { get; set; }
}
@@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript.Functions;
public class TitleDataTechTreeNodeInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("PlayerQuarterLevelRequired")]
public int PlayerQuarterLevelRequired { get; set; }
[JsonPropertyName("NodePerkType")]
public EYTechTreeNodePerkType NodePerkType { get; set; }
[JsonPropertyName("PerkLevels")]
public TitleDataTechTreeNodePerkLevels[] PerkLevels { get; set; }
}
public class TitleDataTechTreeNodePerkLevels {
[JsonPropertyName("PerkAmount")]
public float PerkAmount { get; set; }
[JsonPropertyName("UpgradeCosts")]
public TitleDataTechTreeNodePerkLevelsUpgradeCosts[] UpgradeCosts { get; set; }
[JsonPropertyName("UpgradeSeconds")]
public int UpgradeSeconds { get; set; }
[JsonPropertyName("InitialRushCosts")]
public int InitialRushCosts { get; set; }
[JsonPropertyName("OptionalRushCosts")]
public int OptionalRushCosts { get; set; }
}
public class TitleDataTechTreeNodePerkLevelsUpgradeCosts {
[JsonPropertyName("Currency")]
public string Currency { get; set; }
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
public class TitleDataTechTreeNodeUpgradeDependencies {
[JsonPropertyName("RelatedDependency")]
public string RelatedDependency { get; set; }
[JsonPropertyName("RequiredLevel")]
public int RequiredLevel { get; set; }
}
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
public class TitleDataVanityInfo {
[JsonPropertyName("Name")]
public string Name { get; set; }
[JsonPropertyName("StoreData")]
public TitleDataVanityInfoStoreData StoreData { get; set; }
}
public class TitleDataVanityInfoStoreData {
[JsonPropertyName("Amount")]
public int Amount { get; set; }
}
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
public class UDataTable {
[JsonPropertyName("DataTable")]
public string DataTable { get; set; }
[JsonPropertyName("RowName")]
public string RowName { get; set; }
}
@@ -0,0 +1,28 @@
{
"profiles": {
"Prospect.Server.Api": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production",
"ASPNETCORE_Kestrel__Certificates__Default__Path": ".\\certificate.pfx"
},
"dotnetRunMessages": true,
"applicationUrl": "https://127.0.0.1:8443;http://127.0.0.1:8000",
"useSSL": true
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"sslPort": 0
}
}
}
@@ -1,13 +1,28 @@
{
{
"profiles": {
"Prospect.Server.Api": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": false,
"applicationUrl": "http://localhost:5000;https://localhost:5443",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_Kestrel__Certificates__Default__Path": ".\\certificate.pfx"
},
"dotnetRunMessages": true,
"applicationUrl": "https://127.0.0.1:8443;http://127.0.0.1:8000",
"useSSL": true
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"sslPort": 0
}
}
}
@@ -4,6 +4,23 @@
<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>
@@ -11,10 +11,10 @@ public class AuthTokenService
{
private const string DefaultIssuer = "ProspectApi";
private const string DefaultAudience = "Prospect";
private readonly SymmetricSecurityKey _securityKey;
private readonly JwtSecurityTokenHandler _tokenHandler;
public AuthTokenService(IOptions<AuthTokenSettings> options)
{
_securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(options.Value.Secret));
@@ -59,7 +59,7 @@ public class AuthTokenService
public ClaimsPrincipal Validate(string token)
{
var tokenHandler = new JwtSecurityTokenHandler();
try
{
return tokenHandler.ValidateToken(token, new TokenValidationParameters
@@ -32,7 +32,10 @@ public class CloudScriptService
using (var scope = _serviceScopeFactory.CreateScope())
{
var functionClazz = ActivatorUtilities.CreateInstance(scope.ServiceProvider, function.Clazz);
var functionParam = JsonSerializer.Deserialize(parameter, function.RequestType);
// Some functions have empty parameters which result in malformed JSON.
var parameterJson = parameter == "" ? "{}" : parameter;
// _logger.LogInformation("Received parameter: {Param}", parameterJson);
var functionParam = JsonSerializer.Deserialize(parameterJson, function.RequestType);
if (functionParam == null)
{
_logger.LogWarning("Function {Function} deserialization returned null", name);
@@ -40,9 +43,9 @@ public class CloudScriptService
}
var functionTask = function.Delegate(functionClazz, functionParam);
await functionTask;
return (object)((dynamic)functionTask).Result;
}
}
@@ -0,0 +1,111 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYActivateContractRequest {
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
}
public class FYActivateContractResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("changedCurrencties")]
public object[] ChangedCurrencties { get; set; }
[JsonPropertyName("activatedContract")]
public FYActiveContractPlayerData ActivatedContract { get; set; }
[JsonPropertyName("status")]
public int Status { get; set; }
}
public class FYGetActiveContractsResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("contracts")]
public List<FYActiveContractPlayerData> Contracts { get; set; }
}
[CloudScriptFunction("ActivateContract")]
public class ActivateContract : ICloudScriptFunction<FYActivateContractRequest, FYActivateContractResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public ActivateContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYActivateContractResult> ExecuteAsync(FYActivateContractRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"ContractsActive", "ContractsOneTimeCompleted"});
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)) {
return new FYActivateContractResult
{
UserID = userId,
Error = "Contract not found",
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,
new Dictionary<string, string>{ ["ContractsActive"] = JsonSerializer.Serialize(contractsActive) }
);
return new FYActivateContractResult
{
UserID = userId,
Error = "",
ChangedCurrencties = [], // NOTE: There are no paid contracts in Frontier.
ActivatedContract = contractData,
Status = 14 // EYActivateContractRequestStatus::OK
};
}
}
@@ -0,0 +1,43 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CanGoToStationRequestRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; }
}
public class CanGoToStationRequestResponse
{
[JsonPropertyName("canGoToStation")]
public bool CanGoToStation { get; set; }
[JsonPropertyName("delta")]
public int Delta { get; set; }
[JsonPropertyName("timeStampUpdate")]
public int TimeStampUpdate { get; set; }
}
[CloudScriptFunction("CanGoToStationRequest")]
public class CanGoToStationRequestFunction : ICloudScriptFunction<CanGoToStationRequestRequest, CanGoToStationRequestResponse>
{
private readonly ILogger<CanGoToStationRequestFunction> _logger;
public CanGoToStationRequestFunction(ILogger<CanGoToStationRequestFunction> logger)
{
_logger = logger;
}
public async Task<CanGoToStationRequestResponse> ExecuteAsync(CanGoToStationRequestRequest request)
{
return new CanGoToStationRequestResponse
{
CanGoToStation = true,
Delta = 0,
TimeStampUpdate = 0,
};
}
}
@@ -0,0 +1,65 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYCancelActiveContractRequest {
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
}
public class FYCancelActiveContractResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
}
[CloudScriptFunction("CancelActiveContract")]
public class CancelActiveContract : ICloudScriptFunction<FYCancelActiveContractRequest, FYCancelActiveContractResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public CancelActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYCancelActiveContractResult> ExecuteAsync(FYCancelActiveContractRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"ContractsActive"});
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var contractActiveIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == request.ContractID);
if (contractActiveIdx == -1) {
return new FYCancelActiveContractResult
{
UserID = userId,
Error = "Failed to find active contract",
};
}
contractsActive.Contracts.RemoveAt(contractActiveIdx);
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["ContractsActive"] = JsonSerializer.Serialize(contractsActive) }
);
return new FYCancelActiveContractResult
{
UserID = userId,
Error = "",
ContractID = request.ContractID,
};
}
}
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CheckAndPerformUserRollbackIfNeededRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class CheckAndPerformUserRollbackIfNeededResponse
{
[JsonPropertyName("result")]
public int Result { get; set; }
}
[CloudScriptFunction("CheckAndPerformUserRollbackIfNeeded")]
public class CheckAndPerformUserRollbackIfNeededFunction : ICloudScriptFunction<CheckAndPerformUserRollbackIfNeededRequest, CheckAndPerformUserRollbackIfNeededResponse>
{
private readonly ILogger<CheckAndPerformUserRollbackIfNeededFunction> _logger;
public CheckAndPerformUserRollbackIfNeededFunction(ILogger<CheckAndPerformUserRollbackIfNeededFunction> logger)
{
_logger = logger;
}
public async Task<CheckAndPerformUserRollbackIfNeededResponse> ExecuteAsync(CheckAndPerformUserRollbackIfNeededRequest request)
{
_logger.LogInformation("Processing user rollback if needed");
return new CheckAndPerformUserRollbackIfNeededResponse
{
Result = 1,
};
}
}
@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CheckAndWipeDataForCurrentSeasonRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class CheckAndWipeDataForCurrentSeasonResponse
{
[JsonPropertyName("result")]
public int Result { get; set; }
}
[CloudScriptFunction("CheckAndWipeDataForCurrentSeason")]
public class CheckAndWipeDataForCurrentSeasonFunction : ICloudScriptFunction<CheckAndWipeDataForCurrentSeasonRequest, CheckAndWipeDataForCurrentSeasonResponse>
{
private readonly ILogger<CheckAndWipeDataForCurrentSeasonFunction> _logger;
public CheckAndWipeDataForCurrentSeasonFunction(ILogger<CheckAndWipeDataForCurrentSeasonFunction> logger)
{
_logger = logger;
}
public async Task<CheckAndWipeDataForCurrentSeasonResponse> ExecuteAsync(CheckAndWipeDataForCurrentSeasonRequest request)
{
_logger.LogInformation("Processing check data wipe for current season");
return new CheckAndWipeDataForCurrentSeasonResponse
{
Result = 1,
};
}
}
@@ -0,0 +1,44 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class CheckClientVersionUpToDateRequest
{
[JsonPropertyName("version")]
public string Version { get; set; }
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class CheckClientVersionUpToDateResponse
{
[JsonPropertyName("isClientUpToDate")]
public bool IsClientUpToDate { get; set; }
}
[CloudScriptFunction("CheckClientVersionUpToDate")]
public class CheckClientVersionUpToDateFunction : ICloudScriptFunction<CheckClientVersionUpToDateRequest, CheckClientVersionUpToDateResponse>
{
private readonly ILogger<CheckClientVersionUpToDateFunction> _logger;
public CheckClientVersionUpToDateFunction(ILogger<CheckClientVersionUpToDateFunction> logger)
{
_logger = logger;
}
public async Task<CheckClientVersionUpToDateResponse> ExecuteAsync(CheckClientVersionUpToDateRequest request)
{
_logger.LogInformation("Processing client version check request for client {Version}",
request.Version);
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
};
}
}
@@ -0,0 +1,302 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYClaimCompletedActiveContractRewardsRequest {
[JsonPropertyName("contractId")]
public string ContractID { get; set; }
[JsonPropertyName("contractsToUnlock")]
public string[] ContractsToUnlock { get; set; }
}
public class FYClaimCompletedActiveContractRewardsResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("claimedContractId")]
public string ClaimedContractID { get; set; }
[JsonPropertyName("newContractIdOnBoard")]
public string NewContractIdOnBoard { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("itemsGranted")]
public List<FYCustomItemInfo> ItemsGranted { get; set; }
[JsonPropertyName("itemsUpdatedOrRemoved")]
public List<FYCustomItemInfo> ItemsUpdatedOrRemoved { get; set; }
[JsonPropertyName("playerFactionProgressData")]
public FYPlayerFactionProgressData PlayerFactionProgressData { get; set; }
[JsonPropertyName("contractsActivated")]
public List<FYActiveContractPlayerData> ContractsActivated { get; set; }
[JsonPropertyName("status")]
public int Status { get; set; } // EYClaimContractRewardsStatus
[JsonPropertyName("updatedSeasonXp")]
public int UpdatedSeasonXp { get; set; }
}
public class FYGetCompletedContractsResult {
[JsonPropertyName("contractsIds")]
public List<string> ContractsIDs { get; set; }
};
[CloudScriptFunction("ClaimActiveContract")]
public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveContractRewardsRequest, FYClaimCompletedActiveContractRewardsResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly string[] difficulties = ["Easy", "Medium", "Hard"];
private readonly Random rnd = new Random();
public ClaimActiveContract(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYClaimCompletedActiveContractRewardsResult> ExecuteAsync(FYClaimCompletedActiveContractRewardsRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var titleData = _titleDataService.Find(new List<string>{ "Contracts", "Blueprints", "Jobs", "LevelData" });
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
if (!contracts.TryGetValue(request.ContractID, out var contract)) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "Contract not found",
Status = 13, // EYClaimContractRewardsStatus::WRONG_CONTRACT_ID
};
}
var factionKey = "FactionProgression" + contract.Faction;
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "ContractsOneTimeCompleted", "Balance", "Inventory", factionKey, "JobBoardsData" }
);
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 inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var targetContractIdx = contractsActive.Contracts.FindIndex(item => item.ContractID == contract.Name);
if (targetContractIdx == -1) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "Contract not active",
Status = 8, // EYClaimContractRewardsStatus::NO_ACTIVE_CONTRACT
};
}
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var targetContract = contractsActive.Contracts[targetContractIdx];
List<FYCustomItemInfo> itemsUpdatedOrRemoved = [];
HashSet<string> deletedItemsIds = [];
// TODO: Optimize
for (var i = 0; i < contract.Objectives.Length; i++) {
var objective = contract.Objectives[i];
int remaining = objective.MaxProgress;
if (objective.Type == EYContractObjectiveType.OwnNumOfItem) {
// NOTE: The backend may perform item ownership validation
// since it has the information about the player's items.
foreach (var item in inventory) {
if (item.BaseItemId != objective.ItemToOwn) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
}
itemsUpdatedOrRemoved.Add(item);
if (remaining <= 0) {
break;
}
}
} else if (objective.Type == EYContractObjectiveType.Kills) {
// Kill objectives can't be tracked in a client-hosted raid (no dedicated
// game server reports kills). They are auto-credited on deploy
// (EnterMatchmakingMatch); accept them here even if a contract was
// activated before that so kill contracts remain completable.
remaining = 0;
} else {
// NOTE: A game server must write the correct progress
// for other types of objectives.
// The objectives are mapped by array index.
remaining -= targetContract.Progress[i];
}
if (remaining > 0) {
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "Objective not met",
Status = 16, // EYClaimContractRewardsStatus::PROGRESS_MISSING
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
List<FYCustomItemInfo> itemsGranted = new List<FYCustomItemInfo>();
List<FYCurrencyItem> changedCurrencies = [];
foreach (var reward in contract.Rewards) {
if (reward.ItemID == "SoftCurrency") {
balance.SoftCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (reward.ItemID == "Aurum") {
balance.HardCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else if (reward.ItemID == "InsuranceCurrency") {
balance.InsuranceCurrency += reward.Amount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
} else {
// TODO: Inventory limit check
var blueprintData = blueprints[reward.ItemID];
var remainingAmount = reward.Amount; // Quest rewards are given individually
// Populate incomplete stacks first
foreach (var item in newInventory) {
if (item.BaseItemId != reward.ItemID || item.Amount >= blueprintData.MaxAmountPerStack) {
continue;
}
var remainingSpace = blueprintData.MaxAmountPerStack - item.Amount;
var amountToAdd = Math.Min(remainingAmount, remainingSpace);
item.Amount += amountToAdd;
remainingAmount -= amountToAdd;
itemsUpdatedOrRemoved.Add(item);
if (remainingAmount == 0) {
break;
}
}
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
var itemInfo = new FYCustomItemInfo{
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = reward.ItemID,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
newInventory.Add(itemInfo);
itemsGranted.Add(itemInfo);
remainingAmount -= itemStackAmount;
}
}
}
factionProgression += contract.ReputationIncrease;
contractsActive.Contracts.RemoveAt(targetContractIdx);
// Main contracts can be completed only once.
// 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;
}
// 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,
};
contractsActive.Contracts.Add(newContract);
newContracts.Add(newContract);
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["ContractsOneTimeCompleted"] = JsonSerializer.Serialize(contractsCompleted),
["ContractsActive"] = JsonSerializer.Serialize(contractsActive),
["Balance"] = JsonSerializer.Serialize(balance),
["Inventory"] = JsonSerializer.Serialize(newInventory),
[factionKey] = JsonSerializer.Serialize(factionProgression),
["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData),
}
);
return new FYClaimCompletedActiveContractRewardsResult
{
UserID = userId,
Error = "",
ClaimedContractID = contract.Name,
ContractsActivated = newContracts,
ItemsUpdatedOrRemoved = itemsUpdatedOrRemoved,
ChangedCurrencies = changedCurrencies.ToArray(),
ItemsGranted = itemsGranted,
NewContractIdOnBoard = newContractIdOnBoard,
PlayerFactionProgressData = new FYPlayerFactionProgressData {
FactionID = contract.Faction,
CurrentProgression = factionProgression,
},
Status = 18, // EYClaimContractRewardsStatus::OK
UpdatedSeasonXp = 0 // TODO: Probably a separate season XP rate configured by server?
};
}
}
@@ -0,0 +1,248 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYClaimGeneratorIncomeClientResult {
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("generatorId")]
public string GeneratorID { get; set; }
[JsonPropertyName("changedCurrenciesBalances")]
public FYCurrencyItem[] ChangedCurrenciesBalances { get; set; }
[JsonPropertyName("grantedItems")]
public List<FYCustomItemInfo> GrantedItems { get; set; }
[JsonPropertyName("lastClaimTime")]
public FYTimestamp LastClaimTime { get; set; }
};
public class FYClaimGeneratorIncomeClientRequest {
[JsonPropertyName("generatorId")]
public string GeneratorID { get; set; }
};
public class FYPassiveGenerator {
[JsonPropertyName("generatorId")]
public string GeneratorID { get; set; }
[JsonPropertyName("lastClaimTime")]
public FYTimestamp LastClaimTime { get; set; }
}
[CloudScriptFunction("ClaimGeneratorIncomeClient")]
public class ClaimGeneratorIncomeClient : ICloudScriptFunction<FYClaimGeneratorIncomeClientRequest, FYClaimGeneratorIncomeClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
static readonly Random random = new();
private TitleDataDailyCrateInfo[] PickItems(TitleDataDailyCrateInfo[] items, int numPicks)
{
var itemList = new (TitleDataDailyCrateInfo item, float cumulativeWeight)[items.Length];
float totalWeight = 0;
for (int i = 0; i < items.Length; i++) {
var item = items[i];
totalWeight += item.Weight;
itemList[i] = (item, totalWeight);
}
var selectedItems = new TitleDataDailyCrateInfo[numPicks];
for (int i = 0; i < numPicks; i++) {
float pick = (float)random.NextDouble() * totalWeight;
int left = 0, right = itemList.Length - 1;
while (left < right) {
int mid = (left + right) / 2;
if (itemList[mid].cumulativeWeight < pick) {
left = mid + 1;
} else {
right = mid;
}
}
selectedItems[i] = itemList[left].item;
}
return selectedItems;
}
public ClaimGeneratorIncomeClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYClaimGeneratorIncomeClientResult> ExecuteAsync(FYClaimGeneratorIncomeClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"Generators__2021_09_09", "CharacterTechTreeBonuses", "Balance", "Inventory"}
);
var userGenerators = JsonSerializer.Deserialize<FYPassiveGenerator[]>(userData["Generators__2021_09_09"].Value);
var generator = Array.Find(userGenerators, item => item.GeneratorID == request.GeneratorID);
if (generator == null) {
return new FYClaimGeneratorIncomeClientResult {
UserId = userId,
Error = "Invalid generator ID",
};
}
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var techTreeBonuses = JsonSerializer.Deserialize<CharacterTechTreeBonuses>(userData["CharacterTechTreeBonuses"].Value);
var titleData = _titleDataService.Find(new List<string>{"PassiveGenerators", "DailyCrateRewardsPools", "Blueprints"});
var passiveGenerators = JsonSerializer.Deserialize<Dictionary<string, TitleDataPassiveGeneratorsInfo>>(titleData["PassiveGenerators"]);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
FYCurrencyItem[] changedCurrencies = [];
List<FYCustomItemInfo> grantedItems = [];
switch (generator.GeneratorID) {
case "playerquarters_gen_kmarks": {
var rate = techTreeBonuses.KmarksRate;
if (rate == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "K-Marks Generator Upgrade is not constructed",
};
}
var generatorData = passiveGenerators[generator.GeneratorID];
var hours = (now - generator.LastClaimTime.Seconds) / (60 * generatorData.BaseGenIntervalMinutes);
var cap = techTreeBonuses.KmarksCap + generatorData.BaseCap;
var claimableAmount = Math.Min(hours * rate, cap);
if (claimableAmount == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Generator is not ready yet",
};
}
balance.SoftCurrency += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency }];
break;
}
case "playerquarters_gen_aurum": {
var rate = techTreeBonuses.AurumRate;
if (rate == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Aurum Generator Upgrade is not constructed",
};
}
var generatorData = passiveGenerators[generator.GeneratorID];
var hours = (now - generator.LastClaimTime.Seconds) / (60 * generatorData.BaseGenIntervalMinutes);
var cap = techTreeBonuses.AurumCap + generatorData.BaseCap;
var claimableAmount = (int)Math.Min(hours * rate, cap);
if (claimableAmount == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Generator is not ready yet",
};
}
balance.HardCurrency += claimableAmount;
changedCurrencies = [new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency }];
break;
}
case "playerquarters_gen_crate": {
var crateTier = techTreeBonuses.CrateTier;
if (crateTier == 0) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Crate Generator Upgrade is not constructed",
};
}
var generatorData = passiveGenerators[generator.GeneratorID];
var nextClaimTime = generator.LastClaimTime.Seconds + 60 * generatorData.BaseGenIntervalMinutes;
if (now < nextClaimTime) {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "Generator is not ready yet",
};
}
var dailyCrateRewards = JsonSerializer.Deserialize<TitleDataDailyCrateInfo[][]>(titleData["DailyCrateRewardsPools"]);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var rewardsPool = dailyCrateRewards[crateTier - 1];
// TODO: Get rewardGrants from PassiveGenerators_CrateRewards_DT
var items = PickItems(rewardsPool, 5);
// TODO: Stackable? GrantedItems doesn't imply granted OR updated.
foreach (var item in items) {
var itemInfo = blueprints[item.Name];
var grantedItem = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = item.Amount,
BaseItemId = item.Name,
Durability = itemInfo.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(grantedItem);
grantedItems.Add(grantedItem);
}
break;
}
default: {
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = $"Unknown generator ID: {request.GeneratorID}",
};
}
}
generator.LastClaimTime.Seconds = now;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
["Generators__2021_09_09"] = JsonSerializer.Serialize(userGenerators),
}
);
return new FYClaimGeneratorIncomeClientResult
{
UserId = userId,
Error = "",
GeneratorID = request.GeneratorID,
ChangedCurrenciesBalances = changedCurrencies,
GrantedItems = grantedItems,
LastClaimTime = generator.LastClaimTime,
};
}
}
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class ClientsideFriendsImportRequest
{
[JsonPropertyName("platform")]
public string Platform { get; set; }
[JsonPropertyName("userIds")]
public string[] UserIDs { get; set; }
}
public class ClientsideFriendsImportResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("ClientsideFriendsImport")]
public class ClientsideFriendsImportFunction : ICloudScriptFunction<ClientsideFriendsImportRequest, ClientsideFriendsImportResponse>
{
private readonly ILogger<ClientsideFriendsImportFunction> _logger;
public ClientsideFriendsImportFunction(ILogger<ClientsideFriendsImportFunction> logger)
{
_logger = logger;
}
public async Task<ClientsideFriendsImportResponse> ExecuteAsync(ClientsideFriendsImportRequest request)
{
_logger.LogInformation("Processing friends import");
return new ClientsideFriendsImportResponse{};
}
}
@@ -0,0 +1,154 @@
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,22 +1,38 @@
using Prospect.Server.Api.Services.CloudScript.Models;
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 EnterMatchmaking : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchAzureFunctionResult>
public class EnterMatchmakingFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchAzureFunctionResult>
{
public Task<FYEnterMatchAzureFunctionResult> ExecuteAsync(FYEnterMatchAzureFunction request)
private readonly IHubContext<CycleHub> _hubContext;
public EnterMatchmakingFunction(IHubContext<CycleHub> hubContext)
{
return Task.FromResult(new FYEnterMatchAzureFunctionResult
_hubContext = hubContext;
}
public async Task<FYEnterMatchAzureFunctionResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
//await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
// Success = true,
// SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
// SquadID = request.SquadId,
//});
return new FYEnterMatchAzureFunctionResult
{
Success = true,
Address = "127.0.0.1",
ErrorMessage = "",
Port = 7777,
ShardIndex = 0,
SingleplayerStation = true,
Address = request.MapName,
MaintenanceMode = false,
SessionTimeJoinDelay = 0
});
Port = 7777,
};
}
}
@@ -0,0 +1,114 @@
using System.Text.Json;
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;
public class OnSquadMatchmakingSuccessMessage {
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("squadId")]
public string SquadID { get; set; }
}
[CloudScriptFunction("EnterMatchmakingMatch")]
public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAzureFunction, FYEnterMatchmakingResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHubContext<CycleHub> _hubContext;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public EnterMatchmakingMatchFunction(IHubContext<CycleHub> hubContext, IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_hubContext = hubContext;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYEnterMatchmakingResult> ExecuteAsync(FYEnterMatchAzureFunction request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var titleData = _titleDataService.Find(new List<string>{"Contracts"});
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"ContractsActive", "Inventory"}
);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
// Compute delivery quest progress before deploy.
// This ensures that the quest progress will be shown on the planet.
// The station doesn't seem to care about delivery quests progress and calculates progress
// based on actual items in stash. And so does the "ClaimActiveContract" function.
foreach (var contractActive in contractsActive.Contracts) {
if (!contracts.TryGetValue(contractActive.ContractID, out var contract)) {
continue;
}
for (var i = 0; i < contract.Objectives.Length; i++) {
var objective = contract.Objectives[i];
// Kill objectives: the client-hosted raid has no dedicated game server
// to report per-kill progress to the backend, so kills would never be
// saved. Auto-credit them on deploy so the objective can be completed
// (mirrors the player-kill auto-credit done in ActivateContract).
if (objective.Type == EYContractObjectiveType.Kills) {
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),
}
);
await _hubContext.Clients.All.SendAsync("OnSquadMatchmakingSuccess", new OnSquadMatchmakingSuccessMessage {
Success = true,
SessionID = request.MapName, // TODO: Need to implement TryGetCompleteSquadInfo and pass squad info
SquadID = request.SquadId,
});
return new FYEnterMatchmakingResult
{
Success = true,
ErrorMessage = "",
SingleplayerStation = false,
NumAttempts = 1,
Blocker = 0,
IsMatchTravel = true,
SessionId = "", // TODO: Not sure how this affects match travel
};
}
}
@@ -0,0 +1,107 @@
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;
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)
{
_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
{
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);
}
}
@@ -0,0 +1,34 @@
using Prospect.Server.Api.Services.CloudScript;
public class FetchSteamEntitlementsRequest
{
// Empty request
}
public class FetchSteamEntitlementsResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("FetchSteamEntitlements")]
public class FetchSteamEntitlementsFunction : ICloudScriptFunction<FetchSteamEntitlementsRequest, FetchSteamEntitlementsResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public FetchSteamEntitlementsFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<FetchSteamEntitlementsResponse> ExecuteAsync(FetchSteamEntitlementsRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
return new FetchSteamEntitlementsResponse
{};
}
}
@@ -0,0 +1,52 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class FYActiveGlobalVanity {
[JsonPropertyName("activeGlobalVanityIds")]
public string[] ActiveGlobalVanityIds { get; set; }
[JsonPropertyName("droppodId")]
public string DropPodID { get; set; }
}
public class FYGetActiveGlobalVanityRequest
{
// Empty request
}
public class FYGetActiveGlobalVanityResponse
{
[JsonPropertyName("activeVanity")]
public FYActiveGlobalVanity ActiveVanity { get; set; }
}
[CloudScriptFunction("GetActiveGlobalVanity")]
public class GetActiveGlobalVanityFunction : ICloudScriptFunction<FYGetActiveGlobalVanityRequest, FYGetActiveGlobalVanityResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetActiveGlobalVanityFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYGetActiveGlobalVanityResponse> ExecuteAsync(FYGetActiveGlobalVanityRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"GlobalVanity"});
var activeVanity = JsonSerializer.Deserialize<FYActiveGlobalVanity>(userData["GlobalVanity"].Value);
return new FYGetActiveGlobalVanityResponse{
ActiveVanity = activeVanity
};
}
}
@@ -1,67 +1,75 @@
using Prospect.Server.Api.Services.Auth.Extensions;
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYGetCharacterVanityResponse {
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("returnVanity")]
public FYCharacterVanity ReturnVanity { get; set; }
}
public class FYVanityMaterialItem {
[JsonPropertyName("id")]
public string ID { get; set; }
[JsonPropertyName("materialIndex")]
public int MaterialIndex { get; set; }
}
public class FYCharacterVanity {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("head_item")]
public FYVanityMaterialItem HeadItem { get; set; }
[JsonPropertyName("boots_item")]
public FYVanityMaterialItem BootsItem { get; set; }
[JsonPropertyName("chest_item")]
public FYVanityMaterialItem ChestItem { get; set; }
[JsonPropertyName("glove_item")]
public FYVanityMaterialItem GloveItem { get; set; }
[JsonPropertyName("base_suit_item")]
public FYVanityMaterialItem BaseSuitItem { get; set; }
[JsonPropertyName("melee_weapon_item")]
public FYVanityMaterialItem MeleeWeaponItem { get; set; }
[JsonPropertyName("body_type")]
public int BodyType { get; set; }
[JsonPropertyName("archetype_id")]
public string ArchetypeID { get; set; }
[JsonPropertyName("slot_index")]
public int SlotIndex { get; set; }
}
[CloudScriptFunction("GetCharacterVanity")]
public class GetCharacterVanity : ICloudScriptFunction<FYGetCharacterVanityRequest, object?>
public class GetCharacterVanity : ICloudScriptFunction<FYGetCharacterVanityRequest, FYGetCharacterVanityResponse?>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public GetCharacterVanity(IHttpContextAccessor httpContextAccessor)
public GetCharacterVanity(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public Task<object?> ExecuteAsync(FYGetCharacterVanityRequest request)
public async Task<FYGetCharacterVanityResponse?> ExecuteAsync(FYGetCharacterVanityRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return Task.FromResult<object?>(null);
return null;
}
return Task.FromResult<object?>(new
{
userId = context.User.FindAuthUserId(),
error = (object?)null,
returnVanity = new
{
userId = context.User.FindAuthUserId(),
head_item = new
{
id = "Black02M_Head1",
materialIndex = 0,
},
boots_item = new
{
id = "StarterOutfit01_Boots_M",
materialIndex = 0,
},
chest_item = new
{
id = "StarterOutfit01_Chest_M",
materialIndex = 0,
},
glove_item = new
{
id = "StarterOutfit01_Gloves_M",
materialIndex = 0,
},
base_suit_item = new
{
id = "StarterOutfit01M_BaseSuit",
materialIndex = 0,
},
melee_weapon_item = new
{
id = "Melee_Omega",
materialIndex = 0,
},
body_type = 1,
archetype_id = "TheProspector",
slot_index = 0
}
});
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"CharacterVanity"});
var returnVanity = JsonSerializer.Deserialize<FYCharacterVanity>(userData["CharacterVanity"].Value);
return new FYGetCharacterVanityResponse{
Success = true,
ReturnVanity = returnVanity,
};
}
}
@@ -26,16 +26,7 @@ public class GetCraftingInProgressData : ICloudScriptFunction<FYGetCraftingInPro
{
UserId = context.User.FindAuthUserId(),
Error = string.Empty,
ItemCurrentlyBeingCrafted = new FYItemCurrentlyBeingCrafted
{
ItemId = null,
ItemRarity = -1,
PurchaseAmount = -1,
UtcTimestampWhenCraftingStarted = new FYTimestamp
{
Seconds = 0
}
}
ItemCurrentlyBeingCrafted = {},
});
}
}
@@ -0,0 +1,39 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class GetLoginQueuePositionRequest
{
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
[JsonPropertyName("region")]
public string Region { get; set; }
}
public class GetLoginQueuePositionResponse
{
[JsonPropertyName("waitingUserCount")]
public int WaitingUserCount { get; set; }
}
[CloudScriptFunction("GetLoginQueuePosition")]
public class GetLoginQueuePositionFunction : ICloudScriptFunction<GetLoginQueuePositionRequest, GetLoginQueuePositionResponse>
{
private readonly ILogger<GetLoginQueuePositionFunction> _logger;
public GetLoginQueuePositionFunction(ILogger<GetLoginQueuePositionFunction> logger)
{
_logger = logger;
}
public async Task<GetLoginQueuePositionResponse> ExecuteAsync(GetLoginQueuePositionRequest request)
{
_logger.LogInformation("Processing user rollback if needed");
return new GetLoginQueuePositionResponse
{
WaitingUserCount = 0,
};
}
}
@@ -0,0 +1,77 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
public class GetPlayerQuartersDataClientRequest
{
// Empty request
}
public class FYGetPlayerQuartersDataClientResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("level")]
public int Level { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
}
public class FYPlayerQuarterStatus
{
[JsonPropertyName("level")]
public int Level { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
}
[CloudScriptFunction("GetPlayerQuartersDataClient")]
public class GetPlayerQuartersDataClientFunction : ICloudScriptFunction<GetPlayerQuartersDataClientRequest, FYGetPlayerQuartersDataClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public GetPlayerQuartersDataClientFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYGetPlayerQuartersDataClientResponse> ExecuteAsync(GetPlayerQuartersDataClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"PlayerQuartersLevel"}
);
var titleData = _titleDataService.Find(new List<string>{ "PlayerQuarters" });
var playerQuartersData = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
var userPlayerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var remainingTime = 0;
if (userPlayerQuarters.UpgradeStartedTime.Seconds != 0) {
var nextLevel = playerQuartersData[userPlayerQuarters.Level - 1];
remainingTime = userPlayerQuarters.UpgradeStartedTime.Seconds + nextLevel.UpgradeSeconds - now;
}
return new FYGetPlayerQuartersDataClientResponse {
UserID = userId,
Level = userPlayerQuarters.Level,
RemainingTimeInSeconds = remainingTime,
UpgradeStartedTime = userPlayerQuarters.UpgradeStartedTime,
};
}
}
@@ -0,0 +1,38 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
// ["DropsTotal","DeathsTotal","DeathsPlayerTotal","DeathsCreaturesTotal","DeathsOthersTotal","EvacsTotal","DeathsConsecutive","EvacsConsecutive","KillsPlayersTotal","KillsPlayersMax","KillsCreaturesTotal","KillsCreaturesMax","DamagePlayersTotal","DamagePlayersMax","DamageCreaturesTotal","DamageCreaturesMax","ContractsCompletedKorolevTotal","ContractsCompletedOsirisTotal","ContractsCompletedICATotal","ContractsCompletedTotal","MatchDurationMax","MatchDurationAvg","KMarksMatchMax","KMarksMatchTotal","KMarksTotal"]
public class GetPlayerStatisticsRequestsClientRequest
{
[JsonPropertyName("statistics")]
public string[] Statistics { get; set; }
}
public class GetPlayerStatisticsRequestsClientResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("GetPlayerStatisticsRequestsClient")]
public class GetPlayerStatisticsRequestsClientFunction : ICloudScriptFunction<GetPlayerStatisticsRequestsClientRequest, GetPlayerStatisticsRequestsClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public GetPlayerStatisticsRequestsClientFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<GetPlayerStatisticsRequestsClientResponse> ExecuteAsync(GetPlayerStatisticsRequestsClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
return new GetPlayerStatisticsRequestsClientResponse
{
};
}
}
@@ -1,16 +1,26 @@
using Prospect.Server.Api.Services.CloudScript.Models;
using Microsoft.Extensions.Options;
using Prospect.Server.Api.Config;
using Prospect.Server.Api.Services.CloudScript.Models;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("GetSignalRConnection")]
public class GetSignalRConnection : ICloudScriptFunction<FYGetSignalRConnection, FYGetSignalRConnectionResult>
{
private readonly PlayFabSettings _settings;
public GetSignalRConnection(IOptions<PlayFabSettings> settings)
{
_settings = settings.Value;
}
public Task<FYGetSignalRConnectionResult> ExecuteAsync(FYGetSignalRConnection request)
{
// The game client connects to SignalR only over HTTPS
return Task.FromResult(new FYGetSignalRConnectionResult
{
Url = "https://localhost:5443/signalr/?hub=pubsub",
AccessToken = "TEST"
Url = _settings.SignalRURL,
AccessToken = _settings.SignalRAccessToken
});
}
}
@@ -0,0 +1,69 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Functions;
using Prospect.Server.Api.Services.UserData;
public class GetTechTreeNodeDataClientRequest
{
// Empty request
}
public class GetTechTreeNodeDataClientResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("nodesData")]
public FYTechTreeNodeStatus[] NodesData { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
}
[CloudScriptFunction("GetTechTreeNodeDataClient")]
public class GetTechTreeNodeDataClientFunction : ICloudScriptFunction<GetTechTreeNodeDataClientRequest, GetTechTreeNodeDataClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public GetTechTreeNodeDataClientFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<GetTechTreeNodeDataClientResponse> ExecuteAsync(GetTechTreeNodeDataClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"TechTreeNodeData"}
);
var titleData = _titleDataService.Find(new List<string>{"TechTreeNodes"});
var techTreeNodes = JsonSerializer.Deserialize<Dictionary<string, TitleDataTechTreeNodeInfo>>(titleData["TechTreeNodes"]);
var userTechTree = JsonSerializer.Deserialize<UserTechTreeNodeData>(userData["TechTreeNodeData"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var remainingTime = 0;
if (userTechTree.NodeInProgress != "") {
var nodeInProgress = userTechTree.Nodes[userTechTree.NodeInProgress];
var node = techTreeNodes[userTechTree.NodeInProgress];
remainingTime = nodeInProgress.UpgradeStartedTime.Seconds + node.PerkLevels[nodeInProgress.Level].UpgradeSeconds - now;
}
return new GetTechTreeNodeDataClientResponse
{
UserID = userId,
NodesData = userTechTree.Nodes.Values.ToArray(),
RemainingTimeInSeconds = remainingTime,
};
}
}
@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class YOnLoginRequest
{
[JsonPropertyName("gamePlatform")]
public string GamePlatform { get; set; }
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
[JsonPropertyName("customTags")]
public Dictionary<string, string> CustomTags { get; set; } = new();
}
public class OnLoginResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("OnLogin")]
public class OnLoginFunction : ICloudScriptFunction<YOnLoginRequest, OnLoginResponse>
{
private readonly ILogger<OnLoginFunction> _logger;
public OnLoginFunction(ILogger<OnLoginFunction> logger)
{
_logger = logger;
}
public async Task<OnLoginResponse> ExecuteAsync(YOnLoginRequest request)
{
_logger.LogInformation("Processing login request for platform {Platform} with nonce {Nonce}",
request.GamePlatform, request.LoginNonce);
// Example: Validate login
// if (string.IsNullOrEmpty(request.LoginNonce))
// {
// return new OnLoginResponse
// {};
// }
// Simulating user authentication logic
var sessionToken = Guid.NewGuid().ToString();
return new OnLoginResponse
{};
}
}
@@ -0,0 +1,99 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYPlayerQuarterUpgradePurchaseClientRequest {
// Empty request
}
public class FYPlayerQuarterUpgradePurchaseClientResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("newLevel")]
public int NewLevel { get; set; }
[JsonPropertyName("upgradeStartedTime")]
public FYTimestamp UpgradeStartedTime { get; set; }
[JsonPropertyName("remainingTimeInSeconds")]
public int RemainingTimeInSeconds { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
[JsonPropertyName("deletedItems")]
public string[] DeletedItemsIds { get; set; }
}
[CloudScriptFunction("PlayerQuarterUpgradePurchaseClient")]
public class PlayerQuarterUpgradePurchaseClient : ICloudScriptFunction<FYPlayerQuarterUpgradePurchaseClientRequest, FYPlayerQuarterUpgradePurchaseClientResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public PlayerQuarterUpgradePurchaseClient(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYPlayerQuarterUpgradePurchaseClientResult> ExecuteAsync(FYPlayerQuarterUpgradePurchaseClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"PlayerQuartersLevel"}
);
var userPlayerQuarters = JsonSerializer.Deserialize<FYPlayerQuarterStatus>(userData["PlayerQuartersLevel"].Value);
var titleData = _titleDataService.Find(new List<string>{"PlayerQuarters"});
var playerQuarters = JsonSerializer.Deserialize<TitleDataPlayerQuartersInfo[]>(titleData["PlayerQuarters"]);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var endTime = playerQuarters[userPlayerQuarters.Level - 1].UpgradeSeconds + userPlayerQuarters.UpgradeStartedTime.Seconds;
if (now < endTime) {
return new FYPlayerQuarterUpgradePurchaseClientResult
{
UserID = userId,
Error = "Upgrade is in progress",
};
}
var upgradeStartedTime = new FYTimestamp {
Seconds = 0,
};
userPlayerQuarters.Level++;
userPlayerQuarters.UpgradeStartedTime = upgradeStartedTime;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["PlayerQuartersLevel"] = JsonSerializer.Serialize(userPlayerQuarters),
}
);
return new FYPlayerQuarterUpgradePurchaseClientResult
{
UserID = userId,
Error = "",
NewLevel = userPlayerQuarters.Level,
UpgradeStartedTime = upgradeStartedTime,
RemainingTimeInSeconds = 0,
ChangedItems = [],
DeletedItemsIds = [],
ChangedCurrencies = [],
};
}
}
@@ -0,0 +1,288 @@
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
{
[JsonPropertyName("blueprintName")]
public string BlueprintName { get; set; }
[JsonPropertyName("blueprintRarity")]
public int BlueprintRarity { get; set; }
[JsonPropertyName("purchaseAmount")]
public int PurchaseAmount { get; set; }
[JsonPropertyName("baseItemId")]
public string BaseItemID { get; set; }
[JsonPropertyName("shopItemBelongsTo")]
public string ShopItemBelongsTo { get; set; }
}
public class PurchaseWeaponShopItemResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("itemRarity")]
public int ItemRarity { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("itemsGrantedOrUpdated")]
public List<FYCustomItemInfo> ItemsGrantedOrUpdated { get; set; }
[JsonPropertyName("deletedItemsIds")]
public HashSet<string> DeletedItemsIds { get; set; }
[JsonPropertyName("purchaseAmount")]
public int PurchaseAmount { get; set; }
[JsonPropertyName("blueprintName")]
public string BlueprintName { get; set; }
[JsonPropertyName("shopItemBelongsTo")]
public string ShopItemBelongsTo { get; set; }
}
public class FYCurrencyItem {
[JsonPropertyName("currencyName")]
public string CurrencyName { get; set; }
[JsonPropertyName("amount")]
public int Amount { get; set; }
}
public class FYModItems {
[JsonPropertyName("m")]
public int[] M { get; set; } // mods
};
// FYItemOriginData
public class FYItemOriginBackend {
[JsonPropertyName("t")]
public string T { get; set; } // type
[JsonPropertyName("p")]
public string P { get; set; } // playfabid
[JsonPropertyName("g")]
public string G { get; set; } // guid
};
// FYRolledPerkEntry
public class FYRolledPerkBackend {
[JsonPropertyName("i")]
public int I { get; set; } // short perk id
[JsonPropertyName("r")]
public float R { get; set; } // rolled range value
};
public class FYCustomItemInfo {
[JsonPropertyName("itemId")]
public string ItemId { get; set; }
[JsonPropertyName("baseItemId")]
public string BaseItemId { get; set; }
[JsonPropertyName("primaryVanityId")] // Item skin
public int PrimaryVanityId { get; set; }
[JsonPropertyName("secondaryVanityId")] // Item charm
public int SecondaryVanityId { get; set; }
[JsonPropertyName("amount")]
public int Amount { get; set; }
[JsonPropertyName("durability")]
public int Durability { get; set; }
[JsonPropertyName("modData")]
public FYModItems ModData { get; set; }
[JsonPropertyName("rolledPerks")]
public FYRolledPerkBackend[] RolledPerks { get; set; }
[JsonPropertyName("insurance")]
public string Insurance { get; set; }
[JsonPropertyName("insuranceOwnerPlayfabId")]
public string InsuranceOwnerPlayfabId { get; set; }
[JsonPropertyName("insuredAttachmentId")]
public string InsuredAttachmentId { get; set; }
[JsonPropertyName("origin")]
public FYItemOriginBackend Origin { get; set; }
};
public class CustomVanityItem {
[JsonPropertyName("baseItemId")]
public string BaseItemId { get; set; }
[JsonPropertyName("origin")]
public FYItemOriginBackend Origin { get; set; }
};
[CloudScriptFunction("PurchaseWeaponShopItem")]
public class PurchaseWeaponShopItemFunction : ICloudScriptFunction<PurchaseWeaponShopItemRequest, PurchaseWeaponShopItemResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<PurchaseWeaponShopItemFunction> _logger;
public PurchaseWeaponShopItemFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger<PurchaseWeaponShopItemFunction> logger)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<PurchaseWeaponShopItemResponse> ExecuteAsync(PurchaseWeaponShopItemRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
if (request.PurchaseAmount <= 0) {
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Invalid purchase amount",
};
}
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance", "ContractsOneTimeCompleted"});
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var blueprintsData = _titleDataService.Find(new List<string>{"Blueprints"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(blueprintsData["Blueprints"]);
if (!blueprints.ContainsKey(request.BaseItemID)) {
_logger.LogError("Failed to find item ID {ItemID}", request.BaseItemID);
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Invalid base item ID",
};
}
var blueprintData = blueprints[request.BaseItemID];
// Contract unlock criteria: some items can only be purchased after completing a
// specific contract. Only gate when the item explicitly declares such a lock, so
// items with no lock keep working as before.
if (!string.IsNullOrEmpty(blueprintData.UnlockData?.ContractLockPurchase))
{
var completed = JsonSerializer.Deserialize<FYGetCompletedContractsResult>(userData["ContractsOneTimeCompleted"].Value);
if (completed?.ContractsIDs == null || !completed.ContractsIDs.Contains(blueprintData.UnlockData.ContractLockPurchase))
{
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Item is locked: complete the required contract first",
};
}
}
// Validate price
var shopCraftingData = blueprintData.ItemShopsCraftingData[request.ShopItemBelongsTo];
// TODO: Refactor
List<FYCustomItemInfo> grantedOrUpdated = [];
HashSet<string> deletedItemsIds = [];
List<FYCurrencyItem> changedCurrencies = [];
foreach (var ingredient in shopCraftingData.ItemRecipeIngredients) {
int remaining = ingredient.Amount;
if (ingredient.Currency == "SoftCurrency") {
var buyCost = request.PurchaseAmount * ingredient.Amount;
remaining -= buyCost;
balance.SoftCurrency -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (ingredient.Currency == "Aurum") {
var buyCost = request.PurchaseAmount * ingredient.Amount;
remaining -= buyCost;
balance.HardCurrency -= buyCost;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else {
foreach (var item in inventory) {
if (item.BaseItemId != ingredient.Currency) {
continue;
}
var localRemaining = remaining;
remaining -= item.Amount;
item.Amount -= localRemaining;
if (item.Amount <= 0) {
item.Amount = 0;
deletedItemsIds.Add(item.ItemId);
} else {
grantedOrUpdated.Add(item);
}
if (remaining <= 0) {
break;
}
}
}
if (remaining > 0) {
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "Missing required items",
};
}
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!deletedItemsIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
// TODO: Inventory limit check
// TODO: Refactor into a helper function
var remainingAmount = request.PurchaseAmount * blueprintData.AmountPerPurchase;
// Populate incomplete stacks first
foreach (var item in newInventory) {
if (item.BaseItemId != request.BaseItemID || item.Amount >= blueprintData.MaxAmountPerStack) {
continue;
}
var remainingSpace = blueprintData.MaxAmountPerStack - item.Amount;
var amountToAdd = Math.Min(remainingAmount, remainingSpace);
item.Amount += amountToAdd;
remainingAmount -= amountToAdd;
grantedOrUpdated.Add(item);
if (remainingAmount == 0) {
break;
}
}
// Then create new items
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
var grantedItem = new FYCustomItemInfo {
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = request.BaseItemID,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
newInventory.Add(grantedItem);
grantedOrUpdated.Add(grantedItem);
remainingAmount -= itemStackAmount;
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(newInventory),
["Balance"] = JsonSerializer.Serialize(balance),
}
);
return new PurchaseWeaponShopItemResponse
{
UserID = userId,
Error = "",
BlueprintName = request.BlueprintName,
ItemRarity = request.BlueprintRarity,
PurchaseAmount = request.PurchaseAmount,
ChangedCurrencies = changedCurrencies.ToArray(),
DeletedItemsIds = deletedItemsIds,
ItemsGrantedOrUpdated = grantedOrUpdated,
ShopItemBelongsTo = request.ShopItemBelongsTo
};
}
}
@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RefreshVictimCompensationPackagesRequest
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
}
public class RefreshVictimCompensationPackagesResponse
{
[JsonPropertyName("compensationPackages")]
public object[] CompensationPackages { get; set; }
}
[CloudScriptFunction("RefreshVictimCompensationPackages")]
public class RefreshVictimCompensationPackagesFunction : ICloudScriptFunction<RefreshVictimCompensationPackagesRequest, RefreshVictimCompensationPackagesResponse>
{
private readonly ILogger<RefreshVictimCompensationPackagesFunction> _logger;
public RefreshVictimCompensationPackagesFunction(ILogger<RefreshVictimCompensationPackagesFunction> logger)
{
_logger = logger;
}
public async Task<RefreshVictimCompensationPackagesResponse> ExecuteAsync(RefreshVictimCompensationPackagesRequest request)
{
_logger.LogInformation("Processing refresh victim compensation packages");
return new RefreshVictimCompensationPackagesResponse
{
CompensationPackages = [],
};
}
}
@@ -0,0 +1,33 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RegisterUserMacAddressRequestRequest
{
[JsonPropertyName("macAddress")]
public string MacAddress { get; set; }
}
public class RegisterUserMacAddressRequestResponse
{
[JsonPropertyName("result")]
public bool Result { get; set; }
}
[CloudScriptFunction("RegisterUserMacAddressRequest")]
public class RegisterUserMacAddressRequestFunction : ICloudScriptFunction<RegisterUserMacAddressRequestRequest, RegisterUserMacAddressRequestResponse>
{
private readonly ILogger<RegisterUserMacAddressRequestFunction> _logger;
public RegisterUserMacAddressRequestFunction(ILogger<RegisterUserMacAddressRequestFunction> logger)
{
_logger = logger;
}
public async Task<RegisterUserMacAddressRequestResponse> ExecuteAsync(RegisterUserMacAddressRequestRequest request)
{
return new RegisterUserMacAddressRequestResponse
{
Result = true,
};
}
}
@@ -0,0 +1,103 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYRepairItemRequest {
[JsonPropertyName("instanceId")]
public string InstanceID { get; set; }
}
public class FYRepairItemResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("changedItems")]
public FYCustomItemInfo[] ChangedItems { get; set; }
}
[CloudScriptFunction("RepairItem")]
public class RepairItem : ICloudScriptFunction<FYRepairItemRequest, FYRepairItemResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public RepairItem(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYRepairItemResult> ExecuteAsync(FYRepairItemRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var titleData = _titleDataService.Find(new List<string>{"Blueprints"});
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>{"Balance", "Inventory"}
);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var item = inventory.Find(item => item.ItemId == request.InstanceID);
if (item == null) {
return new FYRepairItemResult
{
UserID = userId,
Error = "Item not found",
};
}
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var blueprintData = blueprints[item.BaseItemId];
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var repairCost = blueprintData.RepairCostBase + (int)Math.Ceiling((1 - (float)item.Durability / blueprintData.DurabilityMax) * blueprintData.RepairCostMaxDurability);
if (item.Durability == 0) {
repairCost += blueprintData.RepairCostModifierBroken;
}
if (balance.SoftCurrency < repairCost) {
return new FYRepairItemResult
{
UserID = userId,
Error = "Insufficient balance",
};
}
balance.SoftCurrency -= repairCost;
item.Durability = blueprintData.DurabilityMax;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
}
);
return new FYRepairItemResult
{
UserID = userId,
Error = "",
ChangedItems = [item],
ChangedCurrencies = [
new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency },
],
};
}
}
@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class ReportBiEventsRequest
{
[JsonPropertyName("messages")]
public object[] Messages { get; set; }
}
public class ReportBiEventsResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("ReportBiEvents")]
public class ReportBiEventsFunction : ICloudScriptFunction<ReportBiEventsRequest, ReportBiEventsResponse>
{
private readonly ILogger<ReportBiEventsFunction> _logger;
public ReportBiEventsFunction(ILogger<ReportBiEventsFunction> logger)
{
_logger = logger;
}
public async Task<ReportBiEventsResponse> ExecuteAsync(ReportBiEventsRequest request)
{
_logger.LogInformation("Processing Bi Event");
return new ReportBiEventsResponse{};
}
}
@@ -0,0 +1,110 @@
using System.Collections;
using System.Diagnostics.Contracts;
using System.Text.Json;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("RequestActiveObjectivesAndBoardsData")]
public class RequestActiveObjectivesAndBoardsData : ICloudScriptFunction<FYQueryFactionProgressionRequest, FYGetPlayerContractsResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
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)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYGetPlayerContractsResult> ExecuteAsync(FYQueryFactionProgressionRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new FYGetPlayerContractsResult{};
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{ "ContractsActive", "JobBoardsData", "FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris" });
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value);
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var lastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc;
var boards = jobBoardsData.Boards;
// The boards are updated once in 3 hours
if (now >= lastBoardRefreshTimeUtc.Seconds + 60 * 60 * 3) {
var titleData = _titleDataService.Find(new List<string> { "Contracts", "Jobs", "LevelData" });
var contracts = JsonSerializer.Deserialize<Dictionary<string, TitleDataContractInfo>>(titleData["Contracts"]);
var jobs = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string[]>>>(titleData["Jobs"]);
var levelsData = JsonSerializer.Deserialize<Dictionary<string, int[]>>(titleData["LevelData"]);
var currentQuests = new Dictionary<string, Dictionary<string, bool>> {
["Korolev"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
["Osiris"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
["ICA"] = new Dictionary<string, bool> { ["Easy"] = false, ["Medium"] = false, ["Hard"] = false },
};
foreach (var contract in contractsActive.Contracts) {
var parts = contract.ContractID.Split('-');
if (parts[0] != "NEW") {
continue;
}
var difficulty = parts[1];
var faction = parts[2] == "KOR" ? "Korolev" : parts[2];
currentQuests[faction][difficulty] = true;
}
for (var i = 0; i < factions.Length; i++) {
var faction = factions[i];
var factionProgression = JsonSerializer.Deserialize<int>(userData[$"FactionProgression{faction}"].Value);
var levels = levelsData[faction];
var level = levels.Length - Array.FindIndex(levels, (int xp) => {
return factionProgression >= xp;
});
for (var j = 0; j < difficulties.Length; j++) {
var difficulty = difficulties[j];
if (currentQuests[faction][difficulty]) {
continue;
}
var factionJobs = jobs[faction];
var jobsAvailable = Array.FindAll(factionJobs[difficulty], (string jobId) => {
return level >= contracts[jobId].UnlockData.Level;
});
string jobId = jobsAvailable.Length > 0 ? jobsAvailable[rnd.Next(jobsAvailable.Length)] : factionJobs[difficulty][0];
boards[i].Contracts[j] = new FYFactionContractData { ContractId = jobId };
}
}
lastBoardRefreshTimeUtc.Seconds = now;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string> { ["JobBoardsData"] = JsonSerializer.Serialize(jobBoardsData) }
);
}
return new FYGetPlayerContractsResult
{
UserId = userId,
Error = null,
ActiveContracts = contractsActive.Contracts,
FactionsContracts = new FYFactionsContractsData
{
Boards = boards,
LastBoardRefreshTimeUtc = lastBoardRefreshTimeUtc
},
RefreshHours24UtcFromBackend = 12 // Doesn't seem to work
};
}
}
@@ -0,0 +1,58 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestCanReconnectRequest
{
// Empty request
}
public class RequestCanReconnectResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
//[JsonPropertyName("error")]
//public int Error { get; set; }
//[JsonPropertyName("changedItems")]
//public int changedItems { get; set; }
//[JsonPropertyName("sessionId")]
//public int SessionID { get; set; }
//[JsonPropertyName("info")]
//public int Info { get; set; }
//[JsonPropertyName("lastSessionResult")]
//public int LastSessionResult { get; set; }
//[JsonPropertyName("lastSessionId")]
//public int LastSessionID { get; set; }
}
[CloudScriptFunction("RequestCanReconnect")]
public class RequestCanReconnectFunction : ICloudScriptFunction<RequestCanReconnectRequest, RequestCanReconnectResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public RequestCanReconnectFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestCanReconnectResponse> ExecuteAsync(RequestCanReconnectRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new RequestCanReconnectResponse
{};
}
return new RequestCanReconnectResponse
{
//Error = 0,
//Info = 0,
//LastSessionResult = 0,
//LastSessionID = 0,
//SessionID = 0,
//changedItems = 0,
UserID = context.User.FindAuthUserId(),
};
}
}
@@ -0,0 +1,165 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class FYFortunaPassClaimedRewards {
[JsonPropertyName("rewardsIds")]
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")]
public string[] RewardsIDs { get; set; }
}
public class RequestClaimFortunaPassRewardsResponse
{
[JsonPropertyName("userIds")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("errorType")]
public int ErrorType { get; set; }
[JsonPropertyName("grantedItems")]
public FYCustomItemInfo[] GrantedItems { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
}
[CloudScriptFunction("RequestClaimFortunaPassRewards")]
public class RequestClaimFortunaPassRewardsFunction : ICloudScriptFunction<RequestClaimFortunaPassRewardsRequest, RequestClaimFortunaPassRewardsResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<RequestClaimFortunaPassRewardsFunction> _logger;
public RequestClaimFortunaPassRewardsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger<RequestClaimFortunaPassRewardsFunction> logger)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<RequestClaimFortunaPassRewardsResponse> ExecuteAsync(RequestClaimFortunaPassRewardsRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// 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;
}
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);
return new RequestClaimFortunaPassRewardsResponse
{
UserId = userId,
ErrorType = 3, // EYFortunaPassToastReponseType::Ok
Error = "",
GrantedItems = grantedItems.ToArray(),
ChangedCurrencies = changedCurrencies.ToArray(),
};
}
}
@@ -1,5 +1,7 @@
using Prospect.Server.Api.Services.Auth.Extensions;
using System.Text.Json;
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;
@@ -7,42 +9,43 @@ 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)
public RequestFactionProgression(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public Task<object?> ExecuteAsync(FYQueryFactionProgressionRequest request)
public async Task<object?> ExecuteAsync(FYQueryFactionProgressionRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return Task.FromResult<object?>(null);
return null;
}
return Task.FromResult<object?>(new
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
{
factions = new []
factions = new[]
{
new
{
factionId = "ICA",
currentProgression = 0
},
new
{
factionId = "Korolev",
currentProgression = 0
},
new
{
factionId = "Osiris",
currentProgression = 0
},
new { factionId = "ICA", currentProgression = Progression("ICA") },
new { factionId = "Korolev", currentProgression = Progression("Korolev") },
new { factionId = "Osiris", currentProgression = Progression("Osiris") },
},
userId = context.User.FindAuthUserId(),
userId,
error = ""
});
};
}
}
}
@@ -0,0 +1,34 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
// Season 1 request
public class RequestIsPlayerStillOnBattleServerRequest
{
}
public class RequestIsPlayerStillOnBattleServerResponse
{
[JsonPropertyName("isStillOnBattleServer")]
public bool IsStillOnBattleServer { get; set; }
[JsonPropertyName("infoStillOnBattleServer")]
public string InfoStillOnBattleServer { get; set; }
}
[CloudScriptFunction("RequestIsPlayerStillOnBattleServer")]
public class RequestIsPlayerStillOnBattleServerFunction : ICloudScriptFunction<RequestIsPlayerStillOnBattleServerRequest, RequestIsPlayerStillOnBattleServerResponse>
{
private readonly ILogger<RequestIsPlayerStillOnBattleServerFunction> _logger;
public RequestIsPlayerStillOnBattleServerFunction(ILogger<RequestIsPlayerStillOnBattleServerFunction> logger)
{
_logger = logger;
}
public async Task<RequestIsPlayerStillOnBattleServerResponse> ExecuteAsync(RequestIsPlayerStillOnBattleServerRequest request)
{
return new RequestIsPlayerStillOnBattleServerResponse
{
IsStillOnBattleServer = false,
InfoStillOnBattleServer = "",
};
}
}
@@ -0,0 +1,31 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RequestMigrateSteamAccountFromCB1Request
{
[JsonPropertyName("steamId")]
public string SteamID { get; set; }
}
public class RequestMigrateSteamAccountFromCB1Response
{}
[CloudScriptFunction("RequestMigrateSteamAccountFromCB1")]
public class RequestMigrateSteamAccountFromCB1Function : ICloudScriptFunction<RequestMigrateSteamAccountFromCB1Request, RequestMigrateSteamAccountFromCB1Response>
{
private readonly ILogger<RequestMigrateSteamAccountFromCB1Function> _logger;
public RequestMigrateSteamAccountFromCB1Function(ILogger<RequestMigrateSteamAccountFromCB1Function> logger)
{
_logger = logger;
}
public async Task<RequestMigrateSteamAccountFromCB1Response> ExecuteAsync(RequestMigrateSteamAccountFromCB1Request request)
{
_logger.LogInformation("Processing CB1 account migration request request for Steam account {SteamID}",
request.SteamID);
return new RequestMigrateSteamAccountFromCB1Response
{};
}
}
@@ -0,0 +1,127 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYRequestMissionCompletedRequest {
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
}
public class FYRequestMissionCompletedResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("rewards")]
public List<FYCustomItemInfo> Rewards { get; set; }
[JsonPropertyName("updatedCurrencies")]
public FYCurrencyItem[] UpdatedCurrencies { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
}
[CloudScriptFunction("RequestMissionCompleted")]
public class RequestMissionCompleted : ICloudScriptFunction<FYRequestMissionCompletedRequest, FYRequestMissionCompletedResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
public RequestMissionCompleted(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public async Task<FYRequestMissionCompletedResult> ExecuteAsync(FYRequestMissionCompletedRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"OnboardingProgression", "Inventory", "Balance"});
var onboardingMission = JsonSerializer.Deserialize<OnboardingMission>(userData["OnboardingProgression"].Value);
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var titleData = _titleDataService.Find(new List<string>{"OnboardingMissions", "OnboardingRewardMissions", "Blueprints"});
var rewards = JsonSerializer.Deserialize<Dictionary<string, TitleDataMissionRewardInfo>>(titleData["OnboardingRewardMissions"]);
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(titleData["Blueprints"]);
var missions = JsonSerializer.Deserialize<Dictionary<string, TitleDataMissionInfo>>(titleData["OnboardingMissions"]);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var currentMission = missions[onboardingMission.CurrentMissionID];
var missionRewardId = currentMission.OnboardingRewards.RowName;
var nextMissionId = currentMission.NextMissionRowHandle.RowName;
onboardingMission.CurrentMissionID = nextMissionId;
onboardingMission.Progress = 0;
List<FYCustomItemInfo> rewardedItems = new List<FYCustomItemInfo>();
List<FYCurrencyItem> changedCurrencies = [];
if (rewards.ContainsKey(missionRewardId)) {
var missionRewards = rewards[missionRewardId].RewardEntries;
foreach (var reward in missionRewards) {
if (reward.RewardRowHandle.RowName == "SoftCurrency") {
balance.SoftCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "SoftCurrency", Amount = balance.SoftCurrency });
} else if (reward.RewardRowHandle.RowName == "Aurum") {
balance.HardCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "Aurum", Amount = balance.HardCurrency });
} else if (reward.RewardRowHandle.RowName == "InsuranceCurrency") {
balance.InsuranceCurrency += reward.RewardAmount;
changedCurrencies.Add(new FYCurrencyItem { CurrencyName = "InsuranceCurrency", Amount = balance.InsuranceCurrency });
} else {
// TODO: Inventory limit check
var blueprintData = blueprints[reward.RewardRowHandle.RowName];
var remainingAmount = reward.RewardAmount * blueprintData.AmountPerPurchase;
while (remainingAmount > 0) {
var itemStackAmount = Math.Min(blueprintData.MaxAmountPerStack, remainingAmount);
// TODO: Mission rewards are probably already given in stacks
var itemInfo = new FYCustomItemInfo{
ItemId = Guid.NewGuid().ToString(),
Amount = itemStackAmount,
BaseItemId = reward.RewardRowHandle.RowName,
Durability = blueprintData.DurabilityMax,
Insurance = "",
InsuranceOwnerPlayfabId = "",
ModData = new FYModItems { M = [] },
Origin = new FYItemOriginBackend { G = "", P = "", T = "" },
InsuredAttachmentId = "",
PrimaryVanityId = 0,
SecondaryVanityId = 0,
RolledPerks = [],
};
inventory.Add(itemInfo);
rewardedItems.Add(itemInfo);
remainingAmount -= itemStackAmount;
}
}
}
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["OnboardingProgression"] = JsonSerializer.Serialize(onboardingMission),
["Inventory"] = JsonSerializer.Serialize(inventory),
["Balance"] = JsonSerializer.Serialize(balance),
}
);
return new FYRequestMissionCompletedResult
{
UserID = userId,
CurrentMissionID = nextMissionId,
Rewards = rewardedItems,
UpdatedCurrencies = changedCurrencies.ToArray(),
Progress = 0
};
}
}
@@ -0,0 +1,83 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYRequestMissionUpdateRequest {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("currentStepName")]
public string CurrentStepName { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
[JsonPropertyName("currentRoomId")]
public string CurrentRoomID { get; set; }
}
public class FYRequestMissionUpdateResult {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("currentMissionId")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("rewards")]
public object[] Rewards { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
}
public class OnboardingMission {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("currentMissionID")]
public string CurrentMissionID { get; set; }
[JsonPropertyName("progress")]
public int Progress { get; set; }
[JsonPropertyName("showPopup")]
public bool ShowPopup { get; set; }
}
[CloudScriptFunction("RequestMissionUpdate")]
public class RequestMissionUpdate : ICloudScriptFunction<FYRequestMissionUpdateRequest, FYRequestMissionUpdateResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestMissionUpdate(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYRequestMissionUpdateResult> ExecuteAsync(FYRequestMissionUpdateRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"OnboardingProgression"});
var onboardingMission = JsonSerializer.Deserialize<OnboardingMission>(userData["OnboardingProgression"].Value);
onboardingMission.CurrentMissionID = request.CurrentMissionID;
onboardingMission.Progress = request.Progress;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["OnboardingProgression"] = JsonSerializer.Serialize(onboardingMission) }
);
return new FYRequestMissionUpdateResult
{
UserID = userId,
Error = "",
CurrentMissionID = request.CurrentMissionID,
Rewards = [],
Progress = request.Progress
};
}
}
@@ -0,0 +1,50 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RequestNewLegalAgreementsRequest
{
[JsonPropertyName("gamePlatform")]
public string GamePlatform { get; set; }
[JsonPropertyName("nDAVersion")]
public int NDAVersion { get; set; }
[JsonPropertyName("eULAVersion")]
public int EULAVersion { get; set; }
[JsonPropertyName("tryBypass")]
public bool TryBypass { get; set; }
[JsonPropertyName("loginNonce")]
public string LoginNonce { get; set; }
}
public class RequestNewLegalAgreementsResponse
{
[JsonPropertyName("requiredNDAVersion")]
public int NDAVersion { get; set; }
[JsonPropertyName("requiredEULAVersion")]
public int EULAVersion { get; set; }
[JsonPropertyName("hasAccepted")]
public int HasAccepted { get; set; }
}
[CloudScriptFunction("RequestNewLegalAgreements")]
public class RequestNewLegalAgreementsFunction : ICloudScriptFunction<RequestNewLegalAgreementsRequest, RequestNewLegalAgreementsResponse>
{
private readonly ILogger<RequestNewLegalAgreementsFunction> _logger;
public RequestNewLegalAgreementsFunction(ILogger<RequestNewLegalAgreementsFunction> logger)
{
_logger = logger;
}
public async Task<RequestNewLegalAgreementsResponse> ExecuteAsync(RequestNewLegalAgreementsRequest request)
{
_logger.LogInformation("Processing request new legal agreements for platform {Platform} with nonce {Nonce}",
request.GamePlatform, request.LoginNonce);
return new RequestNewLegalAgreementsResponse
{
NDAVersion = request.NDAVersion,
EULAVersion = request.EULAVersion,
HasAccepted = 1,
};
}
}
@@ -1,109 +1,119 @@
using Prospect.Server.Api.Services.Auth.Extensions;
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;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
[CloudScriptFunction("RequestPlayerContracts")]
public class RequestPlayerContracts : ICloudScriptFunction<FYGetPlayerContractsRequest, FYGetPlayerContractsResult>
{
private readonly IHttpContextAccessor _httpContextAccessor;
// Board slots are ordered Easy / Medium / Hard (same convention as ClaimActiveContract).
private static readonly string[] Difficulties = ["Easy", "Medium", "Hard"];
private const int RefreshHours = 12;
public RequestPlayerContracts(IHttpContextAccessor httpContextAccessor)
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)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
}
public Task<FYGetPlayerContractsResult> ExecuteAsync(FYGetPlayerContractsRequest request)
public async 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();
return Task.FromResult(new FYGetPlayerContractsResult
var userData = await _userDataService.FindAsync(
userId, userId,
new List<string>
{
"ContractsActive", "JobBoardsData",
"FactionProgressionKorolev", "FactionProgressionICA", "FactionProgressionOsiris",
}
);
var contractsActive = JsonSerializer.Deserialize<FYGetActiveContractsResult>(userData["ContractsActive"].Value)!;
var jobBoardsData = JsonSerializer.Deserialize<JobBoardsData>(userData["JobBoardsData"].Value)!;
var 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))
{
UserId = context.User.FindAuthUserId(),
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,
Error = null,
ActiveContracts = new List<FYActiveContractPlayerData>(),
ActiveContracts = contractsActive.Contracts,
FactionsContracts = new FYFactionsContractsData
{
Boards = new List<FYFactionContractsData>
{
new FYFactionContractsData
{
FactionId = "ICA",
Contracts = new List<FYFactionContractData>
{
new FYFactionContractData
{
ContractId = "NEW-Easy-ICA-Gather-1",
ContractIsLockedDueToLowFactionReputation = false
},
new FYFactionContractData
{
ContractId = "NEW-Medium-ICA-Uplink-1",
ContractIsLockedDueToLowFactionReputation = false
},
new FYFactionContractData
{
ContractId = "NEW-Hard-ICA-Uplink-1",
ContractIsLockedDueToLowFactionReputation = false
}
}
},
new FYFactionContractsData
{
FactionId = "Korolev",
Contracts = new List<FYFactionContractData>
{
new FYFactionContractData
{
ContractId = "NEW-Easy-KOR-Mine-4",
ContractIsLockedDueToLowFactionReputation = false
},
new FYFactionContractData
{
ContractId = "NEW-Medium-KOR-Mine-1",
ContractIsLockedDueToLowFactionReputation = false
},
new FYFactionContractData
{
ContractId = "NEW-Hard-KOR-PvP-6",
ContractIsLockedDueToLowFactionReputation = false
}
}
},
new FYFactionContractsData
{
FactionId = "Osiris",
Contracts = new List<FYFactionContractData>
{
new FYFactionContractData
{
ContractId = "NEW-Easy-Osiris-Brightcaps-1",
ContractIsLockedDueToLowFactionReputation = false
},
new FYFactionContractData
{
ContractId = "NEW-Medium-Osiris-Gather-1",
ContractIsLockedDueToLowFactionReputation = false
},
new FYFactionContractData
{
ContractId = "NEW-Hard-Osiris-Gather-7",
ContractIsLockedDueToLowFactionReputation = false
}
}
}
},
LastBoardRefreshTimeUtc = new FYTimestamp
{
Seconds = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds()
}
Boards = jobBoardsData.Boards,
LastBoardRefreshTimeUtc = jobBoardsData.LastBoardRefreshTimeUtc ?? new FYTimestamp { Seconds = now },
},
RefreshHours24UtcFromBackend = 12
});
RefreshHours24UtcFromBackend = RefreshHours,
};
}
}
}
@@ -0,0 +1,65 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class FYMatchConnectionData {
[JsonPropertyName("addr")]
public string Addr { get; set; }
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("serverId")]
public string ServerID { get; set; }
[JsonPropertyName("connectSinglePlayer")]
public bool ConnectSinglePlayer { get; set; }
[JsonPropertyName("isMatch")]
public bool IsMatch { get; set; }
}
public class RequestServerSessionStateRequest
{
[JsonPropertyName("sessionId")]
public string SessionID { get; set; }
[JsonPropertyName("amountRequests")]
public int AmountRequests { get; set; }
}
public class RequestServerSessionStateResponse
{
[JsonPropertyName("connectionData")]
public FYMatchConnectionData ConnectionData { get; set; }
[JsonPropertyName("retryCounter")]
public int RetryCounter { get; set; }
[JsonPropertyName("canGoToSession")]
public bool CanGoToSession { get; set; }
[JsonPropertyName("shouldCancel")]
public bool ShouldCancel { get; set; }
}
[CloudScriptFunction("RequestServerSessionState")]
public class RequestServerSessionStateFunction : ICloudScriptFunction<RequestServerSessionStateRequest, RequestServerSessionStateResponse>
{
private readonly ILogger<RequestServerSessionStateFunction> _logger;
public RequestServerSessionStateFunction(ILogger<RequestServerSessionStateFunction> logger)
{
_logger = logger;
}
public async Task<RequestServerSessionStateResponse> ExecuteAsync(RequestServerSessionStateRequest request)
{
_logger.LogInformation("Processing server session state");
return new RequestServerSessionStateResponse
{
CanGoToSession = true,
ConnectionData = new FYMatchConnectionData {
Addr = request.SessionID, // FIXME: This is for offline use case only!,
ConnectSinglePlayer = false,
IsMatch = true,
ServerID = "testserver",
SessionID = request.SessionID,
},
ShouldCancel = false,
RetryCounter = 1,
};
}
}
@@ -0,0 +1,81 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestStoreRotationDataRequest
{
// Empty request
}
public class StoreRotationData
{
[JsonPropertyName("storeId")]
public string StoreID { get; set; }
[JsonPropertyName("expirationData")]
public DateTime ExpirationData { get; set; }
}
public class RequestStoreRotationDataResponse
{
[JsonPropertyName("dailyStoreData")]
public StoreRotationData DailyStoreData { get; set; }
[JsonPropertyName("weeklyStoreData")]
public StoreRotationData WeeklyStoreData { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
}
[CloudScriptFunction("RequestStoreRotationData")]
public class RequestStoreRotationDataFunction : ICloudScriptFunction<RequestStoreRotationDataRequest, RequestStoreRotationDataResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public RequestStoreRotationDataFunction(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
// 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;
if (context == null)
{
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}";
return new RequestStoreRotationDataResponse
{
Error = "",
DailyStoreData = new StoreRotationData {
StoreID = dailyStoreId,
ExpirationData = dailyExpiration
},
WeeklyStoreData = new StoreRotationData {
StoreID = weeklyStoreId,
ExpirationData = weeklyExpiration
}
};
}
}
@@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestUpdateAvailableFreeLoadoutRequest
{
}
public class RequestUpdateAvailableFreeLoadoutResponse
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("newRandomSeed")]
public int NewRandomSeed { get; set; }
}
[CloudScriptFunction("RequestUpdateAvailableFreeLoadout")]
public class RequestUpdateAvailableFreeLoadoutFunction : ICloudScriptFunction<RequestUpdateAvailableFreeLoadoutRequest, RequestUpdateAvailableFreeLoadoutResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<RequestUpdateAvailableFreeLoadoutFunction> _logger;
public RequestUpdateAvailableFreeLoadoutFunction(ILogger<RequestUpdateAvailableFreeLoadoutFunction> logger, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestUpdateAvailableFreeLoadoutResponse> ExecuteAsync(RequestUpdateAvailableFreeLoadoutRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
return new RequestUpdateAvailableFreeLoadoutResponse
{
UserId = userId,
Error = "",
NewRandomSeed = 0,
};
}
}
@@ -0,0 +1,44 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
public class RequestUpdateSeasonWipeDataRequest
{
}
public class RequestUpdateSeasonWipeDataResponse
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
}
[CloudScriptFunction("RequestUpdateSeasonWipeData")]
public class RequestUpdateSeasonWipeDataFunction : ICloudScriptFunction<RequestUpdateSeasonWipeDataRequest, RequestUpdateSeasonWipeDataResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<RequestUpdateSeasonWipeDataFunction> _logger;
public RequestUpdateSeasonWipeDataFunction(ILogger<RequestUpdateSeasonWipeDataFunction> logger, IHttpContextAccessor httpContextAccessor)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
}
public async Task<RequestUpdateSeasonWipeDataResponse> ExecuteAsync(RequestUpdateSeasonWipeDataRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
return new RequestUpdateSeasonWipeDataResponse
{
UserId = userId,
Error = "",
};
}
}
@@ -0,0 +1,125 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Models;
using Prospect.Server.Api.Services.UserData;
public class LoadoutData {
[JsonPropertyName("shield")]
public string Shield { get; set; }
[JsonPropertyName("helmet")]
public string Helmet { get; set; }
[JsonPropertyName("weaponOne")]
public string WeaponOne { get; set; }
[JsonPropertyName("weaponTwo")]
public string WeaponTwo { get; set; }
[JsonPropertyName("bag")]
public string Bag { get; set; }
[JsonPropertyName("bagItemsAsJsonStr")]
public string BagItemsAsJsonStr { get; set; }
[JsonPropertyName("safeItemsAsJsonStr")]
public string SafeItemsAsJsonStr { get; set; }
}
public class RequestUpdateStationInventoryRequest
{
[JsonPropertyName("newSet")]
public LoadoutData NewSet { get; set; }
[JsonPropertyName("itemsToAdd")]
public FYCustomItemInfo[] ItemsToAdd { get; set; }
[JsonPropertyName("itemsToUpdateAmount")]
public FYCustomItemInfo[] ItemsToUpdateAmount { get; set; }
[JsonPropertyName("itemsToRemove")]
public HashSet<string> ItemsToRemove { get; set; }
}
public class RequestUpdateStationInventoryResponse
{
// [JsonPropertyName("userId")]
// public string UserId { get; set; }
// [JsonPropertyName("error")]
// public string Error { get; set; }
// [JsonPropertyName("newSet")]
// public LoadoutData NewSet { get; set; }
// [JsonPropertyName("itemsToAdd")]
// public FYCustomItemInfo[] ItemsToAdd { get; set; }
// [JsonPropertyName("itemsToUpdateAmount")]
// public string[] ItemsToUpdateAmount { get; set; }
// [JsonPropertyName("itemsToRemove")]
// public string[] ItemsToRemove { get; set; }
}
[CloudScriptFunction("RequestUpdateStationInventory")]
public class RequestUpdateStationInventoryFunction : ICloudScriptFunction<RequestUpdateStationInventoryRequest, RequestUpdateStationInventoryResponse>
{
private readonly ILogger<RequestUpdateStationInventoryFunction> _logger;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public RequestUpdateStationInventoryFunction(ILogger<RequestUpdateStationInventoryFunction> logger, IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_logger = logger;
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<RequestUpdateStationInventoryResponse> ExecuteAsync(RequestUpdateStationInventoryRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory"});
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
// TODO: Optimize
// TODO: Check deleted items to see if other stacks/mods were updated correctly
// Process inventory update. The player may:
// 1. Split item stack - creates a new item and updates amount of existing item.
// 2. Stack existing items - deletes existing items and updates amount of existing item.
// 3. Change vanity data - updates an existing item.
// 4. Change weapon mods - updates an existing item.
// 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)) {
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 RequestUpdateStationInventoryResponse
{
// UserId = userId,
// Error = "",
// NewSet = request.NewSet,
// ItemsToAdd = [],
// ItemsToRemove = request.ItemsToRemove,
// ItemsToUpdateAmount = request.ItemsToUpdateAmount,
};
}
}
@@ -0,0 +1,45 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class RegionObject
{
[JsonPropertyName("region")]
public string Region { get; set; }
[JsonPropertyName("ping")]
public int Ping { get; set; }
[JsonPropertyName("instanceType")]
public int InstanceType { get; set; }
}
public class RequestUsersPingRequestRequest
{
[JsonPropertyName("pings")]
public RegionObject[] Pings { get; set; }
[JsonPropertyName("countryCode")]
public string CountryCode { get; set; }
}
public class RequestUsersPingRequestResponse
{
// TODO: Unknown structure
}
[CloudScriptFunction("RequestUsersPingRequest")]
public class RequestUsersPingRequestFunction : ICloudScriptFunction<RequestUsersPingRequestRequest, RequestUsersPingRequestResponse>
{
private readonly ILogger<RequestUsersPingRequestFunction> _logger;
public RequestUsersPingRequestFunction(ILogger<RequestUsersPingRequestFunction> logger)
{
_logger = logger;
}
public async Task<RequestUsersPingRequestResponse> ExecuteAsync(RequestUsersPingRequestRequest request)
{
_logger.LogInformation("Processing ping request");
return new RequestUsersPingRequestResponse
{
};
}
}
@@ -0,0 +1,71 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript.Models.Data;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class ResetActionsCapPlayerRequest
{
// Empty request
}
public class ResetActionsCapPlayerResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
}
public class FortunaPassDailyCompletedActions {
[JsonPropertyName("actions")]
public object Actions { get; set; }
[JsonPropertyName("lastDailyCapResetTimeUtc")]
public FYTimestamp LastDailyCapResetTimeUtc { get; set; }
}
[CloudScriptFunction("ResetActionsCapPlayer")]
public class ResetActionsCapPlayer : ICloudScriptFunction<ResetActionsCapPlayerRequest, ResetActionsCapPlayerResponse>
{
private readonly UserDataService _userDataService;
private readonly IHttpContextAccessor _httpContextAccessor;
public ResetActionsCapPlayer(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<ResetActionsCapPlayerResponse> ExecuteAsync(ResetActionsCapPlayerRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"FortunaPass2_DailyCompletedActions"});
var dailyCompletedActions = JsonSerializer.Deserialize<FortunaPassDailyCompletedActions>(userData["FortunaPass2_DailyCompletedActions"].Value);
var now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (now <= dailyCompletedActions.LastDailyCapResetTimeUtc.Seconds + 86400) {
return new ResetActionsCapPlayerResponse {
UserID = userId,
Error = "",
};
}
dailyCompletedActions.LastDailyCapResetTimeUtc.Seconds = now;
await _userDataService.UpdateAsync(userId, userId, new Dictionary<string, string>{
["FortunaPass2_DailyCompletedActions"] = JsonSerializer.Serialize(dailyCompletedActions), // Season 2
["FortunaPass3_DailyCompletedActions"] = JsonSerializer.Serialize(dailyCompletedActions), // Season 3
});
return new ResetActionsCapPlayerResponse{
UserID = userId,
Error = "",
};
}
}
@@ -0,0 +1,174 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Models.Data;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
using Prospect.Server.Api.Utils;
public class SellItemsClientRequest
{
[JsonPropertyName("ids")]
public string[] IDs { get; set; }
[JsonPropertyName("factionId")]
public string FactionID { get; set; }
[JsonPropertyName("inventoryUpdateData")]
public SellItemsClientInventoryData InventoryUpdateData { get; set; }
}
public class SellItemsClientInventoryData {
[JsonPropertyName("itemsToAdd")]
public FYCustomItemInfo[] ItemsToAdd { get; set; }
[JsonPropertyName("itemsToUpdateAmount")]
public FYCustomItemInfo[] ItemsToUpdateAmount { get; set; }
[JsonPropertyName("itemsToRemove")]
public HashSet<string> ItemsToRemove { get; set; }
}
public class SellItemsClientResponse
{
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("error")]
public string Error { get; set; }
[JsonPropertyName("scrappedItemIds")]
public HashSet<string> ScrappedItemIDs { get; set; }
[JsonPropertyName("changedItems")]
public List<FYCustomItemInfo> ChangedItems { get; set; }
[JsonPropertyName("changedCurrencies")]
public FYCurrencyItem[] ChangedCurrencies { get; set; }
[JsonPropertyName("playerFactionProgressionData")]
public FYPlayerFactionProgressData PlayerFactionProgressionData { get; set; }
}
public class FYPlayerFactionProgressData {
[JsonPropertyName("factionId")]
public string FactionID { get; set; }
[JsonPropertyName("currentProgression")]
public int CurrentProgression { get; set; }
}
[CloudScriptFunction("SellItemsClient")]
public class SellItemsClientFunction : ICloudScriptFunction<SellItemsClientRequest, SellItemsClientResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
private readonly TitleDataService _titleDataService;
private readonly ILogger<SellItemsClientFunction> _logger;
public SellItemsClientFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService, TitleDataService titleDataService, ILogger<SellItemsClientFunction> logger)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
_titleDataService = titleDataService;
_logger = logger;
}
public async Task<SellItemsClientResponse> ExecuteAsync(SellItemsClientRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
var progressionFactionKey = $"FactionProgression{request.FactionID}";
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"Inventory", "Balance", progressionFactionKey});
var inventory = JsonSerializer.Deserialize<List<FYCustomItemInfo>>(userData["Inventory"].Value);
var balance = JsonSerializer.Deserialize<PlayerBalance>(userData["Balance"].Value);
var factionProgression = JsonSerializer.Deserialize<int>(userData[progressionFactionKey].Value);
var blueprintsData = _titleDataService.Find(new List<string>{"Blueprints"});
var blueprints = JsonSerializer.Deserialize<Dictionary<string, TitleDataBlueprintInfo>>(blueprintsData["Blueprints"]);
// TODO: Optimize
// TODO: Check deleted items to see if other stacks/mods were updated correctly
// Process inventory update before selling since a player may split item stack.
// This will result in creating a new item and updating amount of existing item.
// NOTE: Stacking items back doesn't seem to work, at least in single-player station. Bug?
// var newInventory = new List<FYCustomItemInfo>(inventory.Count);
// foreach (var item in inventory) {
// if (!request.InventoryUpdateData.ItemsToRemove.Contains(item.ItemId)) {
// newInventory.Add(item);
// }
// }
var changedItems = new List<FYCustomItemInfo>();
foreach (var item in request.InventoryUpdateData.ItemsToUpdateAmount) {
var inventoryItem = inventory.Find(i => i.ItemId == item.ItemId);
if (inventoryItem == null) {
continue;
}
inventoryItem.Amount = item.Amount;
// NOTE: Vanity and mod data cannot be managed in sell menu
changedItems.Add(item);
}
foreach (var item in request.InventoryUpdateData.ItemsToAdd) {
inventory.Add(item);
}
// Then process selling. The game client provides item IDs for newly added items.
HashSet<string> scrappedItemIds = [];
foreach (var itemId in request.IDs) {
var inventoryItem = inventory.Find(i => i.ItemId == itemId);
if (inventoryItem == null) {
continue;
}
var blueprintData = blueprints[inventoryItem.BaseItemId];
// 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 {
factionProgression += blueprintData.OverrideScrappingReputation * inventoryItem.Amount;
balance.SoftCurrency += blueprintData.OverrideScrappingReturns * inventoryItem.Amount;
}
scrappedItemIds.Add(itemId);
}
// TODO: Optimize
var newInventory = new List<FYCustomItemInfo>(inventory.Count);
foreach (var item in inventory) {
if (!scrappedItemIds.Contains(item.ItemId)) {
newInventory.Add(item);
}
}
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{
["Inventory"] = JsonSerializer.Serialize(newInventory),
["Balance"] = JsonSerializer.Serialize(balance),
[progressionFactionKey] = JsonSerializer.Serialize(factionProgression),
}
);
return new SellItemsClientResponse
{
UserID = userId,
Error = "",
ChangedCurrencies = [new FYCurrencyItem {
CurrencyName = "SoftCurrency",
Amount = balance.SoftCurrency,
}],
PlayerFactionProgressionData = new FYPlayerFactionProgressData {
FactionID = request.FactionID,
CurrentProgression = factionProgression,
},
ScrappedItemIDs = scrappedItemIds,
ChangedItems = changedItems,
};
}
}
@@ -0,0 +1,63 @@
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.CloudScript;
public class UserIdentifiers {
[JsonPropertyName("ip")]
public string IP { get; set; }
[JsonPropertyName("platform")]
public string Platform { get; set; }
[JsonPropertyName("sku")]
public string SKU { get; set; }
[JsonPropertyName("oS")]
public string OS { get; set; }
[JsonPropertyName("resolution")]
public string Resolution { get; set; }
[JsonPropertyName("timezone")]
public string Timezone { get; set; }
[JsonPropertyName("language")]
public string Language { get; set; }
}
public class SendGamesightEventRequest
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("revenue_currency")]
public string RevenueCurrency { get; set; }
[JsonPropertyName("revenue_amount")]
public int RevenueAmount { get; set; }
[JsonPropertyName("identifiers")]
public UserIdentifiers Identifiers { get; set; }
}
public class SendGamesightEventResponse
{
// TODO: Unknown structure. Possibly empty response
}
[CloudScriptFunction("SendGamesightEvent")]
public class SendGamesightEventFunction : ICloudScriptFunction<SendGamesightEventRequest, SendGamesightEventResponse>
{
private readonly ILogger<SendGamesightEventFunction> _logger;
public SendGamesightEventFunction(ILogger<SendGamesightEventFunction> logger)
{
_logger = logger;
}
public async Task<SendGamesightEventResponse> ExecuteAsync(SendGamesightEventRequest request)
{
_logger.LogInformation("Processing Gamesight Event");
return new SendGamesightEventResponse{};
}
}
@@ -0,0 +1,52 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.UserData;
public class FYSetActiveGlobalVanityRequest
{
[JsonPropertyName("activeVanity")]
public FYActiveGlobalVanity ActiveVanity { get; set; }
}
public class FYSetActiveGlobalVanityResponse
{
[JsonPropertyName("activeVanity")]
public FYActiveGlobalVanity ActiveVanity { get; set; }
}
[CloudScriptFunction("SetActiveGlobalVanity")]
public class SetActiveGlobalVanityFunction : ICloudScriptFunction<FYSetActiveGlobalVanityRequest, FYSetActiveGlobalVanityResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public SetActiveGlobalVanityFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYSetActiveGlobalVanityResponse> ExecuteAsync(FYSetActiveGlobalVanityRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// TODO: Check vanities availability
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["GlobalVanity"] = JsonSerializer.Serialize(request.ActiveVanity) }
);
return new FYSetActiveGlobalVanityResponse
{
ActiveVanity = request.ActiveVanity,
};
}
}
@@ -0,0 +1,74 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.UserData;
namespace Prospect.Server.Api.Services.CloudScript.Functions;
public class FYSetActiveCharacterArchetypeRequest
{
[JsonPropertyName("archetypeId")]
public string ArcheTypeId { get; set; } // E03_G02
}
public class FYSetActiveCharacterArchetypeResponse
{
[JsonPropertyName("success")]
public bool Success { get; set; }
[JsonPropertyName("returnVanity")]
public FYCharacterVanity ReturnVanity { get; set; }
}
[CloudScriptFunction("SetCharacterVanityArchetype")]
public class SetCharacterVanityArchetype : ICloudScriptFunction<FYSetActiveCharacterArchetypeRequest, FYSetActiveCharacterArchetypeResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public SetCharacterVanityArchetype(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<FYSetActiveCharacterArchetypeResponse> ExecuteAsync(FYSetActiveCharacterArchetypeRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
return new FYSetActiveCharacterArchetypeResponse{};
}
var userId = context.User.FindAuthUserId();
var userData = await _userDataService.FindAsync(userId, userId, new List<string>{"CharacterVanity"});
var returnVanity = JsonSerializer.Deserialize<FYCharacterVanity>(userData["CharacterVanity"].Value);
// TODO: This works only for Season 2. Season 3 has different IDs
var genderSuffix = request.ArcheTypeId.EndsWith("G02") ? "M" : "F";
returnVanity.ArchetypeID = request.ArcheTypeId;
returnVanity.HeadItem.ID = request.ArcheTypeId + "_Head01";
#if SEASON_2_RELEASE || SEASON_2_DEBUG
returnVanity.BootsItem.ID = $"StarterOutfit01_Boots_{genderSuffix}";
returnVanity.ChestItem.ID = $"StarterOutfit01_Chest_{genderSuffix}";
returnVanity.GloveItem.ID = $"StarterOutfit01_Gloves_{genderSuffix}";
#elif SEASON_3_RELEASE || SEASON_3_DEBUG
returnVanity.BootsItem.ID = "StarterOutfit01_Boots";
returnVanity.ChestItem.ID = "StarterOutfit01_Chest";
returnVanity.GloveItem.ID = "StarterOutfit01_Gloves";
#else
#error Unsupported build type
#endif
returnVanity.BaseSuitItem.ID = $"StarterOutfit01{genderSuffix}_BaseSuit";
returnVanity.BodyType = request.ArcheTypeId.EndsWith("G02") ? 1 : 2;
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["CharacterVanity"] = JsonSerializer.Serialize(returnVanity) }
);
return new FYSetActiveCharacterArchetypeResponse
{
Success = true,
ReturnVanity = returnVanity,
};
}
}
@@ -0,0 +1,115 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Prospect.Server.Api.Services.Auth.Extensions;
using Prospect.Server.Api.Services.CloudScript;
using Prospect.Server.Api.Services.CloudScript.Functions;
using Prospect.Server.Api.Services.UserData;
public class VanityDataiD {
[JsonPropertyName("iD")]
public string ID { get; set; }
[JsonPropertyName("materialIndex")]
public int MaterialIndex { get; set; }
}
public class SetCharacterVanityItems {
[JsonPropertyName("userId")]
public string UserID { get; set; }
[JsonPropertyName("head_item")]
public VanityDataiD HeadItem { get; set; }
[JsonPropertyName("boots_item")]
public VanityDataiD BootsItem { get; set; }
[JsonPropertyName("chest_item")]
public VanityDataiD ChestItem { get; set; }
[JsonPropertyName("glove_item")]
public VanityDataiD GloveItem { get; set; }
[JsonPropertyName("base_suit_item")]
public VanityDataiD BaseSuitItem { get; set; }
[JsonPropertyName("melee_weapon_item")]
public VanityDataiD MeleeWeaponItem { get; set; }
[JsonPropertyName("body_type")]
public int BodyType { get; set; }
[JsonPropertyName("archetype_id")]
public string ArchetypeID { get; set; }
[JsonPropertyName("slot_index")]
public int SlotIndex { get; set; }
}
public class SetCharacterVanityItemsRequest {
[JsonPropertyName("desiredVanity")]
public SetCharacterVanityItems DesiredVanity { get; set; }
}
public class SetCharacterVanityItemsResponse
{
[JsonPropertyName("returnVanity")]
public FYCharacterVanity ReturnVanity { get; set; }
}
[CloudScriptFunction("SetCharacterVanityItems")]
public class SetCharacterVanityItemsFunction : ICloudScriptFunction<SetCharacterVanityItemsRequest, SetCharacterVanityItemsResponse>
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly UserDataService _userDataService;
public SetCharacterVanityItemsFunction(IHttpContextAccessor httpContextAccessor, UserDataService userDataService)
{
_httpContextAccessor = httpContextAccessor;
_userDataService = userDataService;
}
public async Task<SetCharacterVanityItemsResponse> ExecuteAsync(SetCharacterVanityItemsRequest request)
{
var context = _httpContextAccessor.HttpContext;
if (context == null)
{
throw new CloudScriptException("CloudScript was not called within a http request");
}
var userId = context.User.FindAuthUserId();
// TODO: Check for available vanity items
// TODO: Refactor
var vanityData = request.DesiredVanity;
var mappedVanityItems = new FYCharacterVanity{
UserID = userId,
BaseSuitItem = new FYVanityMaterialItem{
ID = vanityData.BaseSuitItem.ID,
MaterialIndex = vanityData.BaseSuitItem.MaterialIndex,
},
BootsItem = new FYVanityMaterialItem{
ID = vanityData.BootsItem.ID,
MaterialIndex = vanityData.BootsItem.MaterialIndex,
},
ChestItem = new FYVanityMaterialItem{
ID = vanityData.ChestItem.ID,
MaterialIndex = vanityData.ChestItem.MaterialIndex,
},
GloveItem = new FYVanityMaterialItem{
ID = vanityData.GloveItem.ID,
MaterialIndex = vanityData.GloveItem.MaterialIndex,
},
HeadItem = new FYVanityMaterialItem{
ID = vanityData.HeadItem.ID,
MaterialIndex = vanityData.HeadItem.MaterialIndex,
},
MeleeWeaponItem = new FYVanityMaterialItem{
ID = vanityData.MeleeWeaponItem.ID,
MaterialIndex = vanityData.MeleeWeaponItem.MaterialIndex,
},
ArchetypeID = vanityData.ArchetypeID,
BodyType = vanityData.BodyType,
SlotIndex = vanityData.SlotIndex
};
await _userDataService.UpdateAsync(
userId, userId,
new Dictionary<string, string>{ ["CharacterVanity"] = JsonSerializer.Serialize(mappedVanityItems) }
);
return new SetCharacterVanityItemsResponse
{
ReturnVanity = mappedVanityItems,
};
}
}

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