Skip to content

Commit

Permalink
feat(skymp5-server): add gamemode api methods for master api balance …
Browse files Browse the repository at this point in the history
…manipulation (skyrim-multiplayer#2170)
  • Loading branch information
Pospelove authored Sep 28, 2024
1 parent bd31a54 commit 33c3bda
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 4 deletions.
6 changes: 4 additions & 2 deletions skymp5-server/ts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { System } from "./systems/system";
import { MasterClient } from "./systems/masterClient";
import { Spawn } from "./systems/spawn";
import { Login } from "./systems/login";
import { DiscordBanSystem } from "./systems/discordBanSystem";
import { MasterApiBalanceSystem } from "./systems/masterApiBalanceSystem";
import { EventEmitter } from "events";
import { pid } from "process";
import * as fs from "fs";
Expand All @@ -28,7 +30,6 @@ import * as path from "path";
import * as os from "os";

import * as manifestGen from "./manifestGen";
import { DiscordBanSystem } from "./systems/discordBanSystem";
import { createScampServer } from "./scampNative";

const gamemodeCache = new Map<string, string>();
Expand Down Expand Up @@ -135,7 +136,8 @@ const main = async () => {
new MasterClient(log, port, master, maxPlayers, name, ip, 5000, offlineMode),
new Spawn(log),
new Login(log, maxPlayers, master, port, ip, offlineMode),
new DiscordBanSystem()
new DiscordBanSystem(),
new MasterApiBalanceSystem(log, maxPlayers, master, port, ip, offlineMode)
);

setupStreams(scampNative.getScampNative());
Expand Down
4 changes: 3 additions & 1 deletion skymp5-server/ts/systems/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class Login implements System {
`${this.masterUrl}/api/servers/${this.myAddr}/sessions/${session}`
);
if (!response.data || !response.data.user || !response.data.user.id) {
throw new Error("getUserProfile: bad master-api response");
throw new Error(`getUserProfile: bad master-api response ${JSON.stringify(response.data)}`);
}
return response.data.user as UserProfile;
} catch (error) {
Expand Down Expand Up @@ -83,6 +83,8 @@ export class Login implements System {
this.log("The server is in offline mode, the client is NOT");
} else if (this.offlineMode === false && gameData && gameData.session) {
(async () => {
ctx.gm.emit("userAssignSession", userId, gameData.session);

const guidBeforeAsyncOp = ctx.svr.getUserGuid(userId);
const profile = await this.getUserProfile(gameData.session, userId, ctx);
const guidAfterAsyncOp = ctx.svr.isConnected(userId) ? ctx.svr.getUserGuid(userId) : "<disconnected>";
Expand Down
114 changes: 114 additions & 0 deletions skymp5-server/ts/systems/masterApiBalanceSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { System, Log } from "./system";
import Axios from "axios";
import { SystemContext } from "./system";
import { getMyPublicIp } from "../publicIp";
import { Settings } from "../settings";

export class MasterApiBalanceSystem implements System {
systemName = "MasterApiBalanceSystem";

constructor(
private log: Log,
private maxPlayers: number,
private masterUrl: string | null,
private serverPort: number,
private ip: string,
private offlineMode: boolean) {
this.sessionByUserId = new Array<string | undefined>(this.maxPlayers);
}

async initAsync(ctx: SystemContext): Promise<void> {
const listenerFn = (userId: number, session: string) => {
console.log(`MasterApiBalanceSystem.userAssignSession - Assigning session for userId ${userId}`);
this.sessionByUserId[userId] = session;
};
ctx.gm.on("userAssignSession", listenerFn);

if (this.ip && this.ip != "null") {
this.myAddr = this.ip + ":" + this.serverPort;
} else {
this.myAddr = (await getMyPublicIp()) + ":" + this.serverPort;
}
this.log(
`MasterApiBalanceSystem system assumed that ${this.myAddr} is our address on master`
);

// Effectively makes mp.getUserMasterApiBalance & mp.makeUserMasterApiPurchase a part of gamemode API
(ctx.svr as any).getUserMasterApiBalance = async (userId: number): Promise<number> => {
if (this.offlineMode) {
console.log("MasterApiBalanceSystem.getUserMasterApiBalance - Always zero balance in offline mode");
return 0;
}

const session = this.sessionByUserId[userId];
if (!session) {
console.error(`MasterApiBalanceSystem.getUserMasterApiBalance - Invalid session value for userId ${userId} (session = ${session})`);
throw new Error(`MasterApiBalanceSystem.getUserMasterApiBalance - Invalid session value for userId ${userId} (session = ${session})`);
}
return await this.getUserBalanceImpl(session);
};

(ctx.svr as any).makeUserMasterApiPurchase = async (userId: number, balanceToSpend: number): Promise<{ balanceSpent: number, success: boolean }> => {
if (this.offlineMode) {
console.log("MasterApiBalanceSystem.makeUserMasterApiPurchase - Purchase impossible in offline mode");
return { balanceSpent: 0, success: false };
}
const session = this.sessionByUserId[userId];
if (!session) {
console.error(`MasterApiBalanceSystem.makeUserMasterApiPurchase - Invalid session value for userId ${userId} (session = ${session})`);
throw new Error(`MasterApiBalanceSystem.makeUserMasterApiPurchase - Invalid session value for userId ${userId} (session = ${session})`);
}
return await this.makeUserMasterApiPurchaseImpl(session, balanceToSpend);
};
}

private async getUserBalanceImpl(session: string): Promise<number> {
try {
const response = await Axios.get(
`${this.masterUrl}/api/servers/${this.myAddr}/sessions/${session}/balance`
);
if (!response.data || !response.data.user || !response.data.user.id || typeof response.data.user.balance !== "number") {
throw new Error(`getUserBalanceImpl: bad master-api response ${JSON.stringify(response.data)}`);
}
return response.data.user.balance as number;
} catch (error) {
throw error;
}
}

private async makeUserMasterApiPurchaseImpl(session: string, balanceToSpend: number): Promise<{ balanceSpent: number, success: boolean }> {
try {
const settings = await Settings.get();
const authToken = settings.allSettings.masterApiAuthToken;

if (typeof authToken !== "string" || !authToken) {
throw new Error(`Bad masterApiAuthToken setting: ${authToken}`);
}

const response = await Axios.post(
`${this.masterUrl}/api/servers/${this.myAddr}/sessions/${session}/purchase`,
{ balanceToSpend },
{ headers: { 'X-Auth-Token': authToken } }
);
if (!response.data || typeof response.data.balanceSpent !== "number" || typeof response.data.success !== "boolean") {
throw new Error(`makeUserMasterApiPurchaseImpl: bad master-api response ${JSON.stringify(response.data)}`);
}
return response.data;
} catch (error) {
throw error;
}
}

connect(userId: number, ctx: SystemContext) {
console.log(`MasterApiBalanceSystem.connect - Cleaning session for userId ${userId}`);
this.sessionByUserId[userId] = undefined;
}

disconnect(userId: number, ctx: SystemContext) {
console.log(`MasterApiBalanceSystem.disconnect - Cleaning session for userId ${userId}`);
this.sessionByUserId[userId] = undefined;
}

private myAddr: string;
private sessionByUserId: Array<string | undefined>;
}
2 changes: 1 addition & 1 deletion skymp5-server/ts/systems/spawn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class Spawn implements System {

async initAsync(ctx: SystemContext): Promise<void> {
const settingsObject = await Settings.get();
const listenerFn = (userId: number, userProfileId: number, discordRoleIds: string[], discordId: string | undefined) => {
const listenerFn = (userId: number, userProfileId: number, discordRoleIds: string[], discordId?: string) => {
const { startPoints } = settingsObject;
// TODO: Show race menu if character is not created after relogging
let actorId = ctx.svr.getActorsByProfileId(userProfileId)[0];
Expand Down

0 comments on commit 33c3bda

Please sign in to comment.