Compare commits

...

16 Commits

Author SHA1 Message Date
13c68a75c1 feat: initial stats save (#884)
All checks were successful
Build Docker image / docker (push) Successful in 30s
Build / build (20) (push) Successful in 41s
Build / build (18) (push) Successful in 57s
Build / build (22) (push) Successful in 39s
Closes #203

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/884
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-06 04:42:59 -08:00
8175deb023 chore: get rid of instances of markModified (#908)
All checks were successful
Build / build (20) (push) Successful in 34s
Build / build (22) (push) Successful in 52s
Build Docker image / docker (push) Successful in 31s
Build / build (18) (push) Successful in 1m18s
Closes #904

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/908
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-06 04:39:01 -08:00
1c82b90033 feat: obtaining crewship related items on mission update (#897)
All checks were successful
Build / build (18) (push) Successful in 36s
Build / build (22) (push) Successful in 51s
Build / build (20) (push) Successful in 1m6s
Build Docker image / docker (push) Successful in 36s
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/897
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-05 12:23:35 -08:00
d396fe8b5c fix: handle acquisition of modular weapon parts (#906)
All checks were successful
Build / build (18) (push) Successful in 36s
Build / build (20) (push) Successful in 52s
Build Docker image / docker (push) Successful in 31s
Build / build (22) (push) Successful in 1m30s
Fixes #905

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/906
2025-02-05 09:00:20 -08:00
1351e73961 chore(webui): clarify what credentials are required (#902)
All checks were successful
Build / build (22) (push) Successful in 34s
Build / build (18) (push) Successful in 54s
Build Docker image / docker (push) Successful in 31s
Build / build (20) (push) Successful in 1m11s
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/902
2025-02-05 06:37:31 -08:00
4353c67867 fix: delete inbox messages when deleting account (#899)
All checks were successful
Build / build (18) (push) Successful in 36s
Build / build (22) (push) Successful in 51s
Build / build (20) (push) Successful in 1m3s
Build Docker image / docker (push) Successful in 31s
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/899
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-05 05:54:24 -08:00
8633696dc8 chore: update tunablesController (#901)
All checks were successful
Build / build (18) (push) Successful in 38s
Build / build (20) (push) Successful in 49s
Build / build (22) (push) Successful in 36s
Build Docker image / docker (push) Successful in 1m38s
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/901
2025-02-04 19:13:48 -08:00
a5d74b92c8 feat(import): Consumables (#895)
All checks were successful
Build / build (18) (push) Successful in 35s
Build / build (20) (push) Successful in 53s
Build Docker image / docker (push) Successful in 30s
Build / build (22) (push) Successful in 1m13s
Closes #894

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/895
2025-02-04 09:19:14 -08:00
f15f2bfdbd chore: update favicon (#896)
All checks were successful
Build / build (22) (push) Successful in 34s
Build / build (20) (push) Successful in 51s
Build Docker image / docker (push) Successful in 30s
Build / build (18) (push) Successful in 1m18s
This change is paired with a change in the bootstrapper to make the icons all unique and somewhat resembling their part in the whole.

![image.webp](/attachments/b30a31d9-15bd-4933-93cb-a409a9c91159)

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/896
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-04 06:39:28 -08:00
c1fcd3042e feat(webui): ensure forma count of at least 5 when max ranking item (#893)
All checks were successful
Build / build (22) (push) Successful in 33s
Build / build (20) (push) Successful in 51s
Build Docker image / docker (push) Successful in 30s
Build / build (18) (push) Successful in 1m11s
Closes #889

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/893
2025-02-04 03:22:37 -08:00
fb232f74bd feat: acquiring CrewShipHarness with CrewShip (#888)
All checks were successful
Build / build (20) (push) Successful in 36s
Build / build (22) (push) Successful in 52s
Build / build (18) (push) Successful in 1m5s
Build Docker image / docker (push) Successful in 32s
Closes #886

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/888
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-04 02:29:23 -08:00
c267ce47c3 update docker-compose.yml
All checks were successful
Build / build (20) (push) Successful in 32s
Build Docker image / docker (push) Successful in 31s
Build / build (22) (push) Successful in 1m9s
Build / build (18) (push) Successful in 53s
2025-02-03 22:55:26 +01:00
3537c7e436 add docker workflow using docker hub as remote
All checks were successful
Build / build (18) (push) Successful in 35s
Build / build (20) (push) Successful in 50s
Build Docker image / docker (push) Successful in 30s
Build / build (22) (push) Successful in 1m13s
2025-02-03 22:50:35 +01:00
3b3edaced4 fix: universalPolarityEverywhere not affecting all necramech slots (#891)
All checks were successful
Build / build (18) (push) Successful in 34s
Build / build (20) (push) Successful in 50s
Build / build (22) (push) Successful in 1m12s
Fixes #890

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/891
2025-02-03 13:21:12 -08:00
e46b3c7d29 chore: use mongoose's 'id' function in addGearExpByCategory (#892)
All checks were successful
Build / build (20) (push) Successful in 33s
Build / build (22) (push) Successful in 50s
Build / build (18) (push) Successful in 1m20s
Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/892
2025-02-03 13:20:56 -08:00
241f0c894a chore(webui): remove client cheats (#883)
All checks were successful
Build / build (22) (push) Successful in 33s
Build / build (18) (push) Successful in 53s
Build / build (20) (push) Successful in 1m9s
This has long been only a very small subset of what the bootstrapper offers. I think it's better that the bootstrapper itself provides the interface for it and we don't duplicate the logic so shallowly.

Reviewed-on: http://209.141.38.3/OpenWF/SpaceNinjaServer/pulls/883
2025-02-03 12:10:36 -08:00
22 changed files with 664 additions and 111 deletions

24
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,24 @@
name: Build Docker image
on:
push:
branches:
- main
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up Docker buildx
uses: docker/setup-buildx-action@v3
- name: Log in to container registry
uses: docker/login-action@v3
with:
username: openwf
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: true
tags: |
openwf/spaceninjaserver:latest
openwf/spaceninjaserver:${{ github.sha }}

View File

@ -1,7 +1,7 @@
services:
openwf:
spaceninjaserver:
# build: .
image: ghcr.io/spaceninjaserver/SpaceNinjaServer:latest
image: openwf/spaceninjaserver:latest
environment:
APP_MONGODB_URL: mongodb://openwfagent:spaceninjaserver@mongodb:27017/

View File

@ -12,7 +12,7 @@ export const artifactsController: RequestHandler = async (req, res) => {
const { Upgrade, LevelDiff, Cost, FusionPointCost } = artifactsData;
const inventory = await getInventory(accountId);
const { Upgrades, RawUpgrades } = inventory;
const { Upgrades } = inventory;
const { ItemType, UpgradeFingerprint, ItemId } = Upgrade;
const safeUpgradeFingerprint = UpgradeFingerprint || '{"lvl":0}';
@ -32,13 +32,7 @@ export const artifactsController: RequestHandler = async (req, res) => {
ItemType
}) - 1;
const rawItemIndex = RawUpgrades.findIndex(rawUpgrade => rawUpgrade.ItemType === ItemType);
RawUpgrades[rawItemIndex].ItemCount--;
if (RawUpgrades[rawItemIndex].ItemCount > 0) {
inventory.markModified(`RawUpgrades.${rawItemIndex}.UpgradeFingerprint`);
} else {
RawUpgrades.splice(rawItemIndex, 1);
}
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
if (!config.infiniteCredits) {

View File

@ -195,7 +195,7 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = [];
for (let i = 0; i != 10; ++i) {
for (let i = 0; i != 12; ++i) {
Polarity.push({
Slot: i,
Value: ArtifactPolarity.Any

View File

@ -3,12 +3,23 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { ExportMisc } from "warframe-public-export-plus";
export const addXpController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = req.body as IAddXpRequest;
for (const [category, gear] of Object.entries(request)) {
for (const clientItem of gear) {
const dbItem = inventory[category as TEquipmentKey].id(clientItem.ItemId.$oid);
if (dbItem) {
if (dbItem.ItemType in ExportMisc.uniqueLevelCaps) {
if ((dbItem.Polarized ?? 0) < 5) {
dbItem.Polarized = 5;
}
}
}
}
addGearExpByCategory(inventory, gear, category as TEquipmentKey);
}
await inventory.save();

View File

@ -1,19 +1,23 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { Account } from "@/src/models/loginModel";
import { Inbox } from "@/src/models/inboxModel";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { Ship } from "@/src/models/shipModel";
import { Stats } from "@/src/models/statsModel";
export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
await Promise.all([
Account.deleteOne({ _id: accountId }),
Inbox.deleteMany({ ownerId: accountId }),
Inventory.deleteOne({ accountOwnerId: accountId }),
Loadout.deleteOne({ loadoutOwnerId: accountId }),
PersonalRooms.deleteOne({ personalRoomsOwnerId: accountId }),
Ship.deleteOne({ ShipOwnerId: accountId })
Ship.deleteOne({ ShipOwnerId: accountId }),
Stats.deleteOne({ accountOwnerId: accountId })
]);
res.end();
};

View File

@ -1,15 +1,23 @@
import { RequestHandler } from "express";
// This endpoint is specific to the OpenWF Bootstrapper: https://openwf.io/bootstrapper-manual
interface ITunables {
prohibit_skip_mission_start_timer?: any;
prohibit_fov_override?: any;
prohibit_skip_mission_start_timer?: boolean;
prohibit_fov_override?: boolean;
prohibit_freecam?: boolean;
prohibit_teleport?: boolean;
prohibit_scripts?: boolean;
}
const tunablesController: RequestHandler = (_req, res) => {
const tunablesSet: ITunables = {};
//tunablesSet.prohibit_skip_mission_start_timer = 1;
//tunablesSet.prohibit_fov_override = 1;
res.json(tunablesSet);
const tunables: ITunables = {};
//tunables.prohibit_skip_mission_start_timer = true;
//tunables.prohibit_fov_override = true;
//tunables.prohibit_freecam = true;
//tunables.prohibit_teleport = true;
//tunables.prohibit_scripts = true;
res.json(tunables);
};
export { tunablesController };

View File

@ -1,6 +1,14 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getStats, uploadStats } from "@/src/services/statsService";
import { IStatsUpload } from "@/src/types/statTypes";
import { RequestHandler } from "express";
const uploadController: RequestHandler = (_req, res) => {
const uploadController: RequestHandler = async (req, res) => {
const payload = getJSONfromString<IStatsUpload>(String(req.body));
const accountId = await getAccountIdForRequest(req);
const playerStats = await getStats(accountId);
await uploadStats(playerStats, payload);
res.status(200).end();
};

View File

@ -1,31 +1,33 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IStatsView } from "@/src/types/statTypes";
import { config } from "@/src/services/configService";
import allScans from "@/static/fixed_responses/allScans.json";
import { ExportEnemies } from "warframe-public-export-plus";
import { getInventory } from "@/src/services/inventoryService";
import { getStats } from "@/src/services/statsService";
import { IStatsView } from "@/src/types/statTypes";
const viewController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "XPInfo");
const playerStats = await getStats(accountId);
const responseJson: IStatsView = {};
responseJson.Weapons = [];
const responseJson: IStatsView = playerStats.toJSON();
responseJson.Weapons ??= [];
for (const item of inventory.XPInfo) {
responseJson.Weapons.push({
type: item.ItemType,
xp: item.XP
});
const weaponIndex = responseJson.Weapons.findIndex(element => element.type == item.ItemType);
if (weaponIndex !== -1) {
responseJson.Weapons[weaponIndex].xp == item.XP;
} else {
responseJson.Weapons.push({ type: item.ItemType, xp: item.XP });
}
}
if (config.unlockAllScans) {
const scans = new Set(allScans);
for (const type of Object.keys(ExportEnemies.avatars)) {
if (!scans.has(type)) {
scans.add(type);
}
if (!scans.has(type)) scans.add(type);
}
responseJson.Scans = [];
responseJson.Scans ??= [];
for (const type of scans) {
responseJson.Scans.push({ type: type, scans: 9999 });
}

View File

@ -25,6 +25,7 @@ import {
ISettings,
IInfestedFoundryDatabase,
IHelminthResource,
IMissionDatabase,
IConsumedSuit,
IQuestStage,
IQuestKeyDatabase,
@ -482,6 +483,15 @@ const helminthResourceSchema = new Schema<IHelminthResource>(
{ _id: false }
);
const missionSchema = new Schema<IMissionDatabase>(
{
Tag: String,
Completes: { type: Number, default: 0 },
Tier: { type: Number, required: false }
},
{ _id: false }
);
const questProgressSchema = new Schema<IQuestStage>(
{
c: Number,
@ -997,7 +1007,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
ShipDecorations: [typeCountSchema],
//Railjack/Components(https://warframe.fandom.com/wiki/Railjack/Components)
CrewShipRawSalvage: [Schema.Types.Mixed],
CrewShipRawSalvage: [typeCountSchema],
//Default RailJack
CrewShipAmmo: [typeCountSchema],
@ -1010,7 +1020,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
CrewShipSalvagedWeapons: [Schema.Types.Mixed],
//Complete Mission\Quests
Missions: [Schema.Types.Mixed],
Missions: [missionSchema],
QuestKeys: [questKeysSchema],
ActiveQuest: { type: String, default: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" }, //TODO: check after mission starting gear
//item like DojoKey or Boss missions key

101
src/models/statsModel.ts Normal file
View File

@ -0,0 +1,101 @@
import { Document, Schema, Types, model } from "mongoose";
import { IEnemy, IMission, IScan, ITutorial, IAbility, IWeapon, IStatsDatabase } from "@/src/types/statTypes";
const abilitySchema = new Schema<IAbility>(
{
type: { type: String, required: true },
used: Number
},
{ _id: false }
);
const enemySchema = new Schema<IEnemy>(
{
type: { type: String, required: true },
executions: Number,
headshots: Number,
kills: Number,
assists: Number,
deaths: Number
},
{ _id: false }
);
const missionSchema = new Schema<IMission>(
{
type: { type: String, required: true },
highScore: Number
},
{ _id: false }
);
const scanSchema = new Schema<IScan>(
{
type: { type: String, required: true },
scans: Number
},
{ _id: false }
);
const tutorialSchema = new Schema<ITutorial>(
{
stage: Number
},
{ _id: false }
);
const weaponSchema = new Schema<IWeapon>(
{
type: { type: String, required: true },
equipTime: Number,
hits: Number,
kills: Number,
xp: Number,
assists: Number,
headshots: Number,
fired: Number
},
{ _id: false }
);
const statsSchema = new Schema<IStatsDatabase>({
accountOwnerId: { type: Schema.Types.ObjectId, required: true },
CiphersSolved: Number,
CiphersFailed: Number,
CipherTime: Number,
Weapons: { type: [weaponSchema], default: [] },
Enemies: { type: [enemySchema], default: [] },
MeleeKills: Number,
MissionsCompleted: Number,
MissionsQuit: Number,
MissionsFailed: Number,
TimePlayedSec: Number,
PickupCount: Number,
Tutorial: { type: Map, of: tutorialSchema, default: {} },
Abilities: { type: [abilitySchema], default: [] },
Rating: Number,
Income: Number,
Rank: Number,
PlayerLevel: Number,
Scans: { type: [scanSchema], default: [] },
Missions: { type: [missionSchema], default: [] },
Deaths: Number,
HealCount: Number,
ReviveCount: Number
});
statsSchema.set("toJSON", {
transform(_document, returnedObject) {
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.accountOwnerId;
}
});
export const Stats = model<IStatsDatabase>("Stats", statsSchema);
// eslint-disable-next-line @typescript-eslint/ban-types
export type TStatsDatabaseDocument = Document<unknown, {}, IStatsDatabase> & {
_id: Types.ObjectId;
__v: number;
} & IStatsDatabase;

View File

@ -155,7 +155,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
if (client.Upgrades !== undefined) {
replaceArray<IUpgradeDatabase>(db.Upgrades, client.Upgrades.map(convertUpgrade));
}
for (const key of ["RawUpgrades", "MiscItems"] as const) {
for (const key of ["RawUpgrades", "MiscItems", "Consumables"] as const) {
if (client[key] !== undefined) {
db[key].splice(0, db[key].length);
client[key].forEach(x => {

View File

@ -240,7 +240,14 @@ export const addItem = async (
}
};
} else if (ExportResources[typeName].productCategory == "CrewShips") {
const inventoryChanges = addCrewShip(inventory, typeName);
const inventoryChanges = {
...addCrewShip(inventory, typeName),
// fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest
...(!inventory.CrewShipHarnesses?.length
? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness")
: {})
};
return { InventoryChanges: inventoryChanges };
} else if (ExportResources[typeName].productCategory == "ShipDecorations") {
const changes = [
@ -315,7 +322,6 @@ export const addItem = async (
}
if (typeName in ExportWeapons) {
const weapon = ExportWeapons[typeName];
// Many non-weapon items are "Pistols" in Public Export, so some duck typing is needed.
if (weapon.totalDamage != 0) {
const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName);
updateSlots(inventory, InventorySlot.WEAPONS, 0, 1);
@ -325,6 +331,20 @@ export const addItem = async (
WeaponBin: { count: 1, platinum: 0, Slots: -1 }
}
};
} else {
// Modular weapon parts
const miscItemChanges = [
{
ItemType: typeName,
ItemCount: quantity
} satisfies IMiscItem
];
addMiscItems(inventory, miscItemChanges);
return {
InventoryChanges: {
MiscItems: miscItemChanges
}
};
}
}
if (typeName in creditBundles) {
@ -810,6 +830,17 @@ const addCrewShip = (
return inventoryChanges;
};
const addCrewShipHarness = (
inventory: TInventoryDatabaseDocument,
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const index = inventory.CrewShipHarnesses.push({ ItemType: typeName }) - 1;
inventoryChanges.CrewShipHarnesses ??= [];
(inventoryChanges.CrewShipHarnesses as object[]).push(inventory.CrewShipHarnesses[index].toJSON());
return inventoryChanges;
};
//TODO: wrong id is not erroring
export const addGearExpByCategory = (
inventory: TInventoryDatabaseDocument,
@ -823,12 +854,10 @@ export const addGearExpByCategory = (
return;
}
const itemIndex = ItemId ? category.findIndex(item => item._id?.equals(ItemId.$oid)) : -1;
if (itemIndex !== -1) {
const item = category[itemIndex];
const item = category.id(ItemId.$oid);
if (item) {
item.XP ??= 0;
item.XP += XP;
inventory.markModified(`${categoryName}.${itemIndex}.XP`);
const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == item.ItemType);
if (xpinfoIndex !== -1) {
@ -877,7 +906,6 @@ export const addShipDecorations = (
if (itemIndex !== -1) {
ShipDecorations[itemIndex].ItemCount += ItemCount;
inventory.markModified(`ShipDecorations.${itemIndex}.ItemCount`);
} else {
ShipDecorations.push({ ItemCount, ItemType });
}
@ -892,13 +920,43 @@ export const addConsumables = (inventory: TInventoryDatabaseDocument, itemsArray
if (itemIndex !== -1) {
Consumables[itemIndex].ItemCount += ItemCount;
inventory.markModified(`Consumables.${itemIndex}.ItemCount`);
} else {
Consumables.push({ ItemCount, ItemType });
}
});
};
export const addCrewShipRawSalvage = (
inventory: TInventoryDatabaseDocument,
itemsArray: ITypeCount[] | undefined
): void => {
const { CrewShipRawSalvage } = inventory;
itemsArray?.forEach(({ ItemCount, ItemType }) => {
const itemIndex = CrewShipRawSalvage.findIndex(i => i.ItemType === ItemType);
if (itemIndex !== -1) {
CrewShipRawSalvage[itemIndex].ItemCount += ItemCount;
} else {
CrewShipRawSalvage.push({ ItemCount, ItemType });
}
});
};
export const addCrewShipAmmo = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[] | undefined): void => {
const { CrewShipAmmo } = inventory;
itemsArray?.forEach(({ ItemCount, ItemType }) => {
const itemIndex = CrewShipAmmo.findIndex(i => i.ItemType === ItemType);
if (itemIndex !== -1) {
CrewShipAmmo[itemIndex].ItemCount += ItemCount;
} else {
CrewShipAmmo.push({ ItemCount, ItemType });
}
});
};
export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: ITypeCount[] | undefined): void => {
const { Recipes } = inventory;
@ -907,7 +965,6 @@ export const addRecipes = (inventory: TInventoryDatabaseDocument, itemsArray: IT
if (itemIndex !== -1) {
Recipes[itemIndex].ItemCount += ItemCount;
inventory.markModified(`Recipes.${itemIndex}.ItemCount`);
} else {
Recipes.push({ ItemCount, ItemType });
}
@ -946,7 +1003,6 @@ export const addFusionTreasures = (
if (itemIndex !== -1) {
FusionTreasures[itemIndex].ItemCount += ItemCount;
inventory.markModified(`FusionTreasures.${itemIndex}.ItemCount`);
} else {
FusionTreasures.push({ ItemCount, ItemType, Sockets });
}
@ -1017,7 +1073,6 @@ export const addChallenges = (
if (itemIndex !== -1) {
category[itemIndex].Progress += Progress;
inventory.markModified(`ChallengeProgress.${itemIndex}.ItemCount`);
} else {
category.push({ Name, Progress });
}
@ -1030,7 +1085,6 @@ export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag,
if (itemIndex !== -1) {
Missions[itemIndex].Completes += Completes;
inventory.markModified(`Missions.${itemIndex}.Completes`);
} else {
Missions.push({ Tag, Completes });
}
@ -1046,7 +1100,6 @@ export const addBooster = (ItemType: string, time: number, inventory: TInventory
if (itemIndex !== -1) {
const existingBooster = Boosters[itemIndex];
existingBooster.ExpiryDate = Math.max(existingBooster.ExpiryDate, currentTime) + time;
inventory.markModified(`Boosters.${itemIndex}.ExpiryDate`);
} else {
Boosters.push({ ItemType, ExpiryDate: currentTime + time });
}

View File

@ -7,6 +7,7 @@ import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { Request } from "express";
import { config } from "@/src/services/configService";
import { createStats } from "@/src/services/statsService";
export const isCorrectPassword = (requestPassword: string, databasePassword: string): boolean => {
return requestPassword === databasePassword;
@ -24,6 +25,7 @@ export const createAccount = async (accountData: IDatabaseAccount): Promise<IDat
const shipId = await createShip(account._id);
await createInventory(account._id, { loadOutPresetId: loadoutId, ship: shipId });
await createPersonalRooms(account._id, shipId);
await createStats(account._id.toString());
return account.toJSON();
} catch (error) {
if (error instanceof Error) {

View File

@ -6,6 +6,8 @@ import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/in
import {
addChallenges,
addConsumables,
addCrewShipAmmo,
addCrewShipRawSalvage,
addFocusXpIncreases,
addFusionTreasures,
addGearExpByCategory,
@ -122,6 +124,7 @@ export const addMissionInventoryUpdates = (
addMods(inventory, value);
break;
case "MiscItems":
case "BonusMiscItems":
addMiscItems(inventory, value);
break;
case "Consumables":
@ -136,6 +139,12 @@ export const addMissionInventoryUpdates = (
case "FusionTreasures":
addFusionTreasures(inventory, value);
break;
case "CrewShipRawSalvage":
addCrewShipRawSalvage(inventory, value);
break;
case "CrewShipAmmo":
addCrewShipAmmo(inventory, value);
break;
case "FusionBundles": {
let fusionPoints = 0;
for (const fusionBundle of value) {
@ -195,7 +204,6 @@ export const addMissionRewards = async (
) => {
if (!rewardInfo) {
logger.warn("no reward info provided");
return;
}
//TODO: check double reward merging
@ -287,9 +295,9 @@ function getLevelCreditRewards(nodeName: string): number {
//TODO: get dark sektor fixed credit rewards and railjack bonus
}
function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] {
function getRandomMissionDrops(RewardInfo: IRewardInfo | undefined): IRngResult[] {
const drops: IRngResult[] = [];
if (RewardInfo.node in ExportRegions) {
if (RewardInfo && RewardInfo.node in ExportRegions) {
const region = ExportRegions[RewardInfo.node];
const rewardManifests = region.rewardManifests ?? [];

View File

@ -0,0 +1,283 @@
import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
import { IStatsUpload } from "@/src/types/statTypes";
export const createStats = async (accountId: string): Promise<TStatsDatabaseDocument> => {
const stats = new Stats({ accountOwnerId: accountId });
await stats.save();
return stats;
};
export const getStats = async (accountOwnerId: string): Promise<TStatsDatabaseDocument> => {
let stats = await Stats.findOne({ accountOwnerId: accountOwnerId });
if (!stats) stats = await createStats(accountOwnerId);
return stats;
};
export const uploadStats = async (playerStats: TStatsDatabaseDocument, payload: IStatsUpload): Promise<void> => {
if (payload.add) {
const {
MISSION_COMPLETE,
PICKUP_ITEM,
SCAN,
USE_ABILITY,
FIRE_WEAPON,
HIT_ENTITY_ITEM,
HEADSHOT_ITEM,
KILL_ENEMY_ITEM,
KILL_ENEMY,
EXECUTE_ENEMY,
HEADSHOT,
DIE,
MELEE_KILL,
INCOME,
CIPHER
} = payload.add;
if (MISSION_COMPLETE) {
for (const [key, value] of Object.entries(MISSION_COMPLETE)) {
switch (key) {
case "GS_SUCCESS":
playerStats.MissionsCompleted ??= 0;
playerStats.MissionsCompleted += value;
break;
case "GS_QUIT":
playerStats.MissionsQuit ??= 0;
playerStats.MissionsQuit += value;
break;
case "GS_FAILURE":
playerStats.MissionsFailed ??= 0;
playerStats.MissionsFailed += value;
break;
}
}
}
if (PICKUP_ITEM) {
for (const value of Object.values(PICKUP_ITEM)) {
playerStats.PickupCount ??= 0;
playerStats.PickupCount += value;
}
}
if (SCAN) {
playerStats.Scans ??= [];
for (const [key, scans] of Object.entries(SCAN)) {
const scan = playerStats.Scans.find(element => element.type === key);
if (scan) {
scan.scans ??= 0;
scan.scans += scans;
} else {
playerStats.Scans.push({ type: key, scans });
}
}
}
if (USE_ABILITY) {
playerStats.Abilities ??= [];
for (const [key, used] of Object.entries(USE_ABILITY)) {
const ability = playerStats.Abilities.find(element => element.type === key);
if (ability) {
ability.used ??= 0;
ability.used += used;
} else {
playerStats.Abilities.push({ type: key, used });
}
}
}
if (FIRE_WEAPON) {
playerStats.Weapons ??= [];
for (const [key, fired] of Object.entries(FIRE_WEAPON)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.fired ??= 0;
weapon.fired += fired;
} else {
playerStats.Weapons.push({ type: key, fired });
}
}
}
if (HIT_ENTITY_ITEM) {
playerStats.Weapons ??= [];
for (const [key, hits] of Object.entries(HIT_ENTITY_ITEM)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.hits ??= 0;
weapon.hits += hits;
} else {
playerStats.Weapons.push({ type: key, hits });
}
}
}
if (HEADSHOT_ITEM) {
playerStats.Weapons ??= [];
for (const [key, headshots] of Object.entries(HEADSHOT_ITEM)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.headshots ??= 0;
weapon.headshots += headshots;
} else {
playerStats.Weapons.push({ type: key, headshots });
}
}
}
if (KILL_ENEMY_ITEM) {
playerStats.Weapons ??= [];
for (const [key, kills] of Object.entries(KILL_ENEMY_ITEM)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.kills ??= 0;
weapon.kills += kills;
} else {
playerStats.Weapons.push({ type: key, kills });
}
}
}
if (KILL_ENEMY) {
playerStats.Enemies ??= [];
for (const [key, kills] of Object.entries(KILL_ENEMY)) {
const enemy = playerStats.Enemies.find(element => element.type === key);
if (enemy) {
enemy.kills ??= 0;
enemy.kills += kills;
} else {
playerStats.Enemies.push({ type: key, kills });
}
}
}
if (EXECUTE_ENEMY) {
playerStats.Enemies ??= [];
for (const [key, executions] of Object.entries(EXECUTE_ENEMY)) {
const enemy = playerStats.Enemies.find(element => element.type === key);
if (enemy) {
enemy.executions ??= 0;
enemy.executions += executions;
} else {
playerStats.Enemies.push({ type: key, executions });
}
}
}
if (HEADSHOT) {
playerStats.Enemies ??= [];
for (const [key, headshots] of Object.entries(HEADSHOT)) {
const enemy = playerStats.Enemies.find(element => element.type === key);
if (enemy) {
enemy.headshots ??= 0;
enemy.headshots += headshots;
} else {
playerStats.Enemies.push({ type: key, headshots });
}
}
}
if (DIE) {
playerStats.Enemies ??= [];
for (const [key, deaths] of Object.entries(DIE)) {
playerStats.Deaths ??= 0;
playerStats.Deaths += deaths;
const enemy = playerStats.Enemies.find(element => element.type === key);
if (enemy) {
enemy.deaths ??= 0;
enemy.deaths += deaths;
} else {
playerStats.Enemies.push({ type: key, deaths });
}
}
}
if (MELEE_KILL) {
playerStats.MeleeKills ??= 0;
for (const kills of Object.values(MELEE_KILL)) {
playerStats.MeleeKills += kills;
}
}
if (INCOME) {
playerStats.Income ??= 0;
playerStats.Income += INCOME;
}
if (CIPHER) {
if (CIPHER["0"] > 0) {
playerStats.CiphersFailed ??= 0;
playerStats.CiphersFailed += CIPHER["0"];
}
if (CIPHER["1"] > 0) {
playerStats.CiphersSolved ??= 0;
playerStats.CiphersSolved += CIPHER["1"];
}
}
}
if (payload.timers) {
const { EQUIP_WEAPON, CURRENT_MISSION_TIME, CIPHER_TIME } = payload.timers;
if (EQUIP_WEAPON) {
playerStats.Weapons ??= [];
for (const [key, equipTime] of Object.entries(EQUIP_WEAPON)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.equipTime ??= 0;
weapon.equipTime += equipTime;
} else {
playerStats.Weapons.push({ type: key, equipTime });
}
}
}
if (CURRENT_MISSION_TIME) {
playerStats.TimePlayedSec ??= 0;
playerStats.TimePlayedSec += CURRENT_MISSION_TIME;
}
if (CIPHER_TIME) {
playerStats.CipherTime ??= 0;
playerStats.CipherTime += CIPHER_TIME;
}
}
if (payload.max) {
const { WEAPON_XP, MISSION_SCORE } = payload.max;
if (WEAPON_XP) {
playerStats.Weapons ??= [];
for (const [key, xp] of Object.entries(WEAPON_XP)) {
const weapon = playerStats.Weapons.find(element => element.type === key);
if (weapon) {
weapon.xp = xp;
} else {
playerStats.Weapons.push({ type: key, xp });
}
}
}
if (MISSION_SCORE) {
playerStats.Missions ??= [];
for (const [key, highScore] of Object.entries(MISSION_SCORE)) {
const mission = playerStats.Missions.find(element => element.type === key);
if (mission) {
mission.highScore = highScore;
} else {
playerStats.Missions.push({ type: key, highScore });
}
}
}
}
if (payload.set) {
const { ELO_RATING, RANK, PLAYER_LEVEL } = payload.set;
if (ELO_RATING) playerStats.Rating = ELO_RATING;
if (RANK) playerStats.Rank = RANK;
if (PLAYER_LEVEL) playerStats.PlayerLevel = PLAYER_LEVEL;
}
await playerStats.save();
};

View File

@ -753,10 +753,13 @@ export interface ILotusCustomization extends IItemConfig {
Persona: string;
}
export interface IMission {
export interface IMissionDatabase {
Tag: string;
Completes: number;
Tier?: number;
Tag: string;
}
export interface IMission extends IMissionDatabase {
RewardsCooldownTime?: IMongoDate;
}

View File

@ -35,18 +35,23 @@ export interface IUpdateChallengeProgressRequest {
}
export type IMissionInventoryUpdateRequest = {
MiscItems?: ITypeCount[];
Recipes?: ITypeCount[];
FusionBundles?: ITypeCount[];
Consumables?: ITypeCount[];
FusionBundels?: ITypeCount[];
CrewShipRawSalvage?: ITypeCount[];
CrewShipAmmo?: ITypeCount[];
BonusMiscItems?: ITypeCount[];
AffiliationChanges?: IAffiliationChange[];
crossPlaySetting?: string;
rewardsMultiplier?: number;
GoalTag: string;
LevelKeyName: string;
ActiveBoosters?: IBooster[];
FusionBundles?: ITypeCount[];
RawUpgrades?: IRawUpgrade[];
MiscItems?: ITypeCount[];
Consumables?: ITypeCount[];
FusionTreasures?: IFusionTreasure[];
Recipes?: ITypeCount[];
QuestKeys?: Omit<IQuestKeyDatabase, "CompletionDate">[];
RegularCredits?: number;
MissionFailed: boolean;

View File

@ -1,3 +1,5 @@
import { Types } from "mongoose";
export interface IStatsView {
CiphersSolved?: number;
CiphersFailed?: number;
@ -23,28 +25,32 @@ export interface IStatsView {
ReviveCount?: number;
}
export interface IStatsDatabase extends IStatsView {
accountOwnerId: Types.ObjectId;
}
export interface IAbility {
used: number;
type: string;
used: number;
}
export interface IEnemy {
type: string;
executions?: number;
headshots?: number;
kills?: number;
type: string;
assists?: number;
deaths?: number;
}
export interface IMission {
highScore: number;
type: string;
highScore: number;
}
export interface IScan {
scans: number;
type: string;
scans: number;
}
export interface ITutorial {
@ -52,12 +58,91 @@ export interface ITutorial {
}
export interface IWeapon {
type: string;
equipTime?: number;
hits?: number;
kills?: number;
xp?: number;
assists?: number;
type: string;
headshots?: number;
fired?: number;
}
export interface IStatsUpload {
displayName: string;
guildId?: string;
PS?: string;
add?: IStatsAdd;
set?: IStatsSet;
max?: IStatsMax;
timers?: IStatsTimers;
}
export interface IStatsAdd {
GEAR_USED?: IUploadEntry;
SCAN?: IUploadEntry;
MISSION_COMPLETE?: IUploadEntry;
HEADSHOT_ITEM?: IUploadEntry;
HEADSHOT?: IUploadEntry;
PLAYER_COUNT?: IUploadEntry;
HOST_MIGRATION?: IUploadEntry;
PICKUP_ITEM?: IUploadEntry;
FIRE_WEAPON?: IUploadEntry;
HIT_ENTITY_ITEM?: IUploadEntry;
DESTROY_DECORATION?: IUploadEntry;
KILL_ENEMY?: IUploadEntry;
TAKE_DAMAGE?: IUploadEntry;
SQUAD_KILL_ENEMY?: IUploadEntry;
RECEIVE_UPGRADE?: IUploadEntry;
USE_ABILITY?: IUploadEntry;
SQUAD_VIP_KILL?: IUploadEntry;
HEAL_BUDDY?: IUploadEntry;
INCOME?: number;
CIPHER?: IUploadEntry;
EQUIP_COSMETIC?: IUploadEntry;
EQUIP_UPGRADE?: IUploadEntry;
KILL_BOSS?: IUploadEntry;
MISSION_TYPE?: IUploadEntry;
MISSION_FACTION?: IUploadEntry;
MISSION_PLAYED?: IUploadEntry;
MISSION_PLAYED_TIME?: IUploadEntry;
MEDALS_TOP?: IUploadEntry;
INPUT_ACTIVITY_TIME?: IUploadEntry;
KILL_ENEMY_ITEM?: IUploadEntry;
TAKE_DAMAGE_ITEM?: IUploadEntry;
SQUAD_KILL_ENEMY_ITEM?: IUploadEntry;
MELEE_KILL?: IUploadEntry;
SQUAD_MELEE_KILL?: IUploadEntry;
MELEE_KILL_ITEM?: IUploadEntry;
SQUAD_MELEE_KILL_ITEM?: IUploadEntry;
DIE?: IUploadEntry;
DIE_ITEM?: IUploadEntry;
EXECUTE_ENEMY?: IUploadEntry;
EXECUTE_ENEMY_ITEM?: IUploadEntry;
}
export interface IUploadEntry {
[key: string]: number;
}
export interface IStatsMax {
WEAPON_XP?: IUploadEntry;
MISSION_SCORE?: IUploadEntry;
}
export interface IStatsSet {
ELO_RATING?: number;
RANK?: number;
PLAYER_LEVEL?: number;
}
export interface IStatsTimers {
IN_SHIP_TIME?: number;
IN_SHIP_VIEW_TIME?: IUploadEntry;
EQUIP_WEAPON?: IUploadEntry;
MISSION_TIME?: IUploadEntry;
REGION_TIME?: IUploadEntry;
PLATFORM_TIME?: IUploadEntry;
CURRENT_MISSION_TIME?: number;
CIPHER_TIME?: number;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -73,7 +73,7 @@
</div>
<div class="w-100">
<div data-route="/webui/" data-title="Login | OpenWF WebUI">
<p>Login using your OpenWF account credentials.</p>
<p>Login using your OpenWF account credentials (same as in-game when connecting to this server).</p>
<form onsubmit="doLogin();return false;">
<label for="email">Email address</label>
<input class="form-control" type="email" id="email" required />
@ -368,7 +368,7 @@
</div>
<div data-route="/webui/cheats, /webui/settings" data-title="Cheats | OpenWF WebUI">
<div class="row g-3">
<div class="col-lg-4">
<div class="col-md-6">
<div class="card mb-3">
<h5 class="card-header">Server</h5>
<div class="card-body">
@ -479,7 +479,7 @@
</div>
</div>
</div>
<div class="col-lg-4">
<div class="col-md-6">
<div class="card mb-3">
<h5 class="card-header">Account</h5>
<div class="card-body">
@ -488,26 +488,6 @@
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-3">
<h5 class="card-header">Client</h5>
<div id="client-cheats-nok" class="card-body">
Client cheats are currently unavailable. This could be because your client is not running or using a DLL without an HTTP interface.
</div>
<div id="client-cheats-ok" class="card-body d-none">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="skip_mission_start_timer" />
<label class="form-check-label" for="skip_mission_start_timer">
Skip Mission Start Timer
</label>
</div>
<div class="form-group mt-3">
<label class="form-label" for="fov_override">FOV Override (0 to disable)</label>
<input id="fov_override" class="form-range" type="range" min="0" value="0" max="2260000" step="10000">
</div>
</div>
</div>
</div>
</div>
</div>
<div data-route="/webui/import" data-title="Import | OpenWF WebUI">

View File

@ -915,34 +915,6 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
});
}
}, 10);
fetch("http://localhost:61558/ping", { mode: "no-cors" })
.then(() => {
$("#client-cheats-ok").removeClass("d-none");
$("#client-cheats-nok").addClass("d-none");
fetch("http://localhost:61558/skip_mission_start_timer")
.then(res => res.text())
.then(res => {
document.getElementById("skip_mission_start_timer").checked = res == "1";
});
document.getElementById("skip_mission_start_timer").onchange = function () {
fetch("http://localhost:61558/skip_mission_start_timer?" + this.checked);
};
fetch("http://localhost:61558/fov_override")
.then(res => res.text())
.then(res => {
document.getElementById("fov_override").value = parseFloat(res) * 10000;
});
document.getElementById("fov_override").oninput = function () {
fetch("http://localhost:61558/fov_override?" + this.value);
};
})
.catch(function () {
$("#client-cheats-nok").removeClass("d-none");
$("#client-cheats-ok").addClass("d-none");
});
});
function doUnlockAllFocusSchools() {