mirror of
https://github.com/thepeacockproject/Peacock
synced 2025-02-23 03:35:25 +01:00
New saving/loading behaviors to fix bugs related to saving (#73)
Signed-off-by: moonysolari <118079569+moonysolari@users.noreply.github.com>
This commit is contained in:
parent
d95553bd33
commit
e548325955
@ -23,6 +23,7 @@ import { serializeSession, deserializeSession } from "./sessionSerialization"
|
||||
import { castUserProfile } from "./utils"
|
||||
import { getConfig } from "./configSwizzleManager"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { unlink, readdir } from "fs/promises"
|
||||
|
||||
/**
|
||||
* Container for functions that handle file read/writes,
|
||||
@ -216,17 +217,24 @@ export async function writeExternalUserData(
|
||||
/**
|
||||
* Reads a contract session from the contractSessions folder.
|
||||
*
|
||||
* @param sessionId The ID of the session to load.
|
||||
* @param identifier The identifier for the saved session, in the format of token_sessionID.
|
||||
* @returns The contract session.
|
||||
*/
|
||||
export async function getContractSession(
|
||||
sessionId: string,
|
||||
identifier: string,
|
||||
): Promise<ContractSession> {
|
||||
const files = await readdir("contractSessions")
|
||||
const filtered = files.filter((fn) => fn.endsWith(`_${identifier}.json`))
|
||||
|
||||
if (filtered.length === 0) {
|
||||
throw new Error(`No session saved with identifier ${identifier}`)
|
||||
}
|
||||
|
||||
// The filtered files have the same identifier, they are just stored at different slots
|
||||
// So we can read any of them and it will be the same.
|
||||
return deserializeSession(
|
||||
JSON.parse(
|
||||
(
|
||||
await readFile(join("contractSessions", `${sessionId}.json`))
|
||||
).toString(),
|
||||
(await readFile(join("contractSessions", filtered[0]))).toString(),
|
||||
),
|
||||
)
|
||||
}
|
||||
@ -234,15 +242,25 @@ export async function getContractSession(
|
||||
/**
|
||||
* Writes a contract session to the contractsSessions folder.
|
||||
*
|
||||
* @param sessionId The session's ID.
|
||||
* @param identifier The identifier for the saved session, in the format of slot_token_sessionID.
|
||||
* @param session The contract session.
|
||||
*/
|
||||
export async function writeContractSession(
|
||||
sessionId: string,
|
||||
identifier: string,
|
||||
session: ContractSession,
|
||||
): Promise<void> {
|
||||
return await writeFile(
|
||||
join("contractSessions", `${sessionId}.json`),
|
||||
join("contractSessions", `${identifier}.json`),
|
||||
JSON.stringify(serializeSession(session)),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a saved contract session from the contractsSessions folder.
|
||||
*
|
||||
* @param fileName The identifier for the saved session, in the format of slot_token_sessionID.
|
||||
* @throws ENOENT if the file is not found.
|
||||
*/
|
||||
export async function deleteContractSession(fileName: string): Promise<void> {
|
||||
return await unlink(join("contractSessions", `${fileName}.json`))
|
||||
}
|
||||
|
@ -33,19 +33,13 @@ import {
|
||||
import { extractToken, ServerVer } from "./utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import {
|
||||
getContractSession,
|
||||
getUserData,
|
||||
writeContractSession,
|
||||
writeUserData,
|
||||
} from "./databaseHandler"
|
||||
import { getUserData, writeUserData } from "./databaseHandler"
|
||||
import { controller } from "./controller"
|
||||
import { swapToLocationStatus } from "./discordRp"
|
||||
import { randomUUID } from "crypto"
|
||||
import { liveSplitManager } from "./livesplit/liveSplitManager"
|
||||
import { handleMultiplayerEvent } from "./multiplayer/multiplayerService"
|
||||
import { handleEvent } from "@peacockproject/statemachine-parser"
|
||||
import picocolors from "picocolors"
|
||||
import { encodePushMessage } from "./multiplayer/multiplayerUtils"
|
||||
import {
|
||||
ActorTaggedC2SEvent,
|
||||
@ -65,6 +59,7 @@ import {
|
||||
SpottedC2SEvent,
|
||||
WitnessesC2SEvent,
|
||||
} from "./types/events"
|
||||
import picocolors from "picocolors"
|
||||
|
||||
const eventRouter = Router()
|
||||
|
||||
@ -682,14 +677,20 @@ function saveEvents(
|
||||
case "IntroCutEnd":
|
||||
if (!session.timerStart) {
|
||||
session.timerStart = event.Timestamp
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Mission started at: ${session.timerStart}`,
|
||||
)
|
||||
}
|
||||
break
|
||||
case "exit_gate":
|
||||
session.timerEnd = event.Timestamp
|
||||
log(LogLevel.DEBUG, `Mission ended at: ${session.timerEnd}`)
|
||||
break
|
||||
case "ContractEnd":
|
||||
if (!session.timerEnd) {
|
||||
session.timerEnd = event.Timestamp
|
||||
log(LogLevel.DEBUG, `Mission ended at: ${session.timerEnd}`)
|
||||
}
|
||||
break
|
||||
case "ObjectiveCompleted":
|
||||
@ -799,31 +800,4 @@ function saveEvents(
|
||||
return response
|
||||
}
|
||||
|
||||
export async function saveSession(
|
||||
sessionId: string,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
if (!contractSessions.has(sessionId)) {
|
||||
log(LogLevel.WARN, `Refusing to save ${sessionId} as it doesn't exist`)
|
||||
return
|
||||
}
|
||||
|
||||
await writeContractSession(
|
||||
token + "_" + sessionId,
|
||||
contractSessions.get(sessionId)!,
|
||||
)
|
||||
}
|
||||
|
||||
export async function loadSession(
|
||||
sessionId: string,
|
||||
token: string,
|
||||
sessionData?: ContractSession,
|
||||
): Promise<void> {
|
||||
if (!sessionData) {
|
||||
sessionData = await getContractSession(token + "_" + sessionId)
|
||||
}
|
||||
|
||||
contractSessions.set(sessionId, sessionData)
|
||||
}
|
||||
|
||||
export { eventRouter }
|
||||
|
@ -21,20 +21,24 @@ import path from "path"
|
||||
import { castUserProfile, nilUuid, uuidRegex } from "./utils"
|
||||
import { json as jsonMiddleware } from "body-parser"
|
||||
import { getPlatformEntitlements } from "./platformEntitlements"
|
||||
import {
|
||||
getActiveSessionIdForUser,
|
||||
loadSession,
|
||||
newSession,
|
||||
saveSession,
|
||||
} from "./eventHandler"
|
||||
import { contractSessions, newSession } from "./eventHandler"
|
||||
import type {
|
||||
CompiledChallengeRuntimeData,
|
||||
ContractSession,
|
||||
GameVersion,
|
||||
RequestWithJwt,
|
||||
SaveFile,
|
||||
UpdateUserSaveFileTableBody,
|
||||
UserProfile,
|
||||
} from "./types/types"
|
||||
import { log, LogLevel } from "./loggingInterop"
|
||||
import { getUserData, writeUserData } from "./databaseHandler"
|
||||
import {
|
||||
deleteContractSession,
|
||||
getContractSession,
|
||||
getUserData,
|
||||
writeContractSession,
|
||||
writeUserData,
|
||||
} from "./databaseHandler"
|
||||
import { randomUUID } from "crypto"
|
||||
import { getVersionedConfig } from "./configSwizzleManager"
|
||||
import { createInventory } from "./inventory"
|
||||
@ -630,26 +634,38 @@ profileRouter.post(
|
||||
profileRouter.post(
|
||||
"/ProfileService/UpdateUserSaveFileTable",
|
||||
jsonMiddleware(),
|
||||
async (req, res) => {
|
||||
async (req: RequestWithJwt<never, UpdateUserSaveFileTableBody>, res) => {
|
||||
if (req.body.clientSaveFileList.length > 0) {
|
||||
const save =
|
||||
req.body.clientSaveFileList[
|
||||
req.body.clientSaveFileList.length - 1
|
||||
]
|
||||
// We are saving to the SaveFile with the most recent timestamp.
|
||||
// Others are ignored.
|
||||
const save: SaveFile = req.body.clientSaveFileList.reduce(
|
||||
(prev: SaveFile, current: SaveFile) =>
|
||||
prev.TimeStamp > current.TimeStamp ? prev : current,
|
||||
)
|
||||
const userData = getUserData(req.jwt.unique_name, req.gameVersion)
|
||||
|
||||
try {
|
||||
await saveSession(
|
||||
save.ContractSessionId,
|
||||
save.Value.LastEventToken,
|
||||
)
|
||||
await saveSession(save, userData)
|
||||
// Successfully saved, so edit user data
|
||||
if (!userData.Extensions.Saves) {
|
||||
userData.Extensions.Saves = {}
|
||||
}
|
||||
userData.Extensions.Saves[save.Value.Name] = {
|
||||
Timestamp: save.TimeStamp,
|
||||
ContractSessionId: save.ContractSessionId,
|
||||
Token: save.Value.LastEventToken,
|
||||
}
|
||||
writeUserData(req.jwt.unique_name, req.gameVersion)
|
||||
} catch (e) {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Unable to save session ${save?.ContractSessionId}`,
|
||||
)
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(LogLevel.DEBUG, e.name)
|
||||
if (getErrorCause(e) === "cause uninvestigated") {
|
||||
log(LogLevel.DEBUG, `${getErrorMessage(e)}`)
|
||||
} else {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
`Unable to save session ${
|
||||
save?.ContractSessionId
|
||||
} because ${getErrorMessage(e)}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -658,6 +674,75 @@ profileRouter.post(
|
||||
},
|
||||
)
|
||||
|
||||
function getErrorMessage(error: unknown) {
|
||||
if (error instanceof Error) return error.message
|
||||
return String(error)
|
||||
}
|
||||
|
||||
function getErrorCause(error: unknown) {
|
||||
if (error instanceof Error) return error.cause
|
||||
return String(error)
|
||||
}
|
||||
|
||||
async function saveSession(
|
||||
save: SaveFile,
|
||||
userData: UserProfile,
|
||||
): Promise<void> {
|
||||
const sessionId = save.ContractSessionId
|
||||
const token = save.Value.LastEventToken
|
||||
const slot = save.Value.Name
|
||||
|
||||
if (!contractSessions.has(sessionId)) {
|
||||
throw new Error("the session does not exist in the server's memory", {
|
||||
cause: "non-existent",
|
||||
})
|
||||
}
|
||||
if (!userData.Extensions.Saves) {
|
||||
userData.Extensions.Saves = {}
|
||||
}
|
||||
if (slot in userData.Extensions.Saves) {
|
||||
const delta = save.TimeStamp - userData.Extensions.Saves[slot].Timestamp
|
||||
|
||||
if (delta === 0) {
|
||||
throw new Error(
|
||||
`the client is accessing /ProfileService/UpdateUserSaveFileTable with nothing updated.`,
|
||||
{ cause: "cause uninvestigated" },
|
||||
)
|
||||
} else if (delta < 0) {
|
||||
throw new Error(`there is a newer save in slot ${slot}`, {
|
||||
cause: "outdated",
|
||||
})
|
||||
} else {
|
||||
// If we can delete the old save, then do it. If not, we can still proceed.
|
||||
try {
|
||||
await deleteContractSession(
|
||||
slot +
|
||||
"_" +
|
||||
userData.Extensions.Saves[slot].Token +
|
||||
"_" +
|
||||
userData.Extensions.Saves[slot].ContractSessionId,
|
||||
)
|
||||
} catch (e) {
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Failed to delete old ${slot} save. ${getErrorMessage(e)}.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await writeContractSession(
|
||||
slot + "_" + token + "_" + sessionId,
|
||||
contractSessions.get(sessionId)!,
|
||||
)
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Saved contract to slot ${slot} with token = ${token}, session id = ${sessionId}, start time = ${
|
||||
contractSessions.get(sessionId).timerStart
|
||||
}.`,
|
||||
)
|
||||
}
|
||||
|
||||
profileRouter.post(
|
||||
"/ContractSessionsService/Load",
|
||||
jsonMiddleware(),
|
||||
@ -674,47 +759,59 @@ profileRouter.post(
|
||||
try {
|
||||
await loadSession(req.body.contractSessionId, req.body.saveToken)
|
||||
} catch (e) {
|
||||
if (
|
||||
getActiveSessionIdForUser(req.jwt.unique_name) ===
|
||||
req.body.contractSessionId
|
||||
) {
|
||||
log(
|
||||
LogLevel.INFO,
|
||||
"Tried to load the active session, prevented to avoid crash.",
|
||||
)
|
||||
} else {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"No such save detected! Might be an official servers save.",
|
||||
)
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
|
||||
)
|
||||
}
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Failed to load contract with token = ${req.body.saveToken}, session id = ${req.body.contractSessionId} because ${e.message}`,
|
||||
)
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"No such save detected! Might be an official servers save.",
|
||||
)
|
||||
|
||||
if (PEACOCK_DEV) {
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"Creating a fake session to avoid problems... scoring will not work!",
|
||||
)
|
||||
|
||||
newSession(
|
||||
req.body.contractSessionId,
|
||||
req.body.contractId,
|
||||
req.jwt.unique_name,
|
||||
req.body.difficultyLevel!,
|
||||
req.gameVersion,
|
||||
false,
|
||||
LogLevel.DEBUG,
|
||||
`(Save-context: ${req.body.contractSessionId}; ${req.body.saveToken})`,
|
||||
)
|
||||
}
|
||||
|
||||
log(
|
||||
LogLevel.WARN,
|
||||
"Creating a fake session to avoid problems... scoring will not work!",
|
||||
)
|
||||
|
||||
newSession(
|
||||
req.body.contractSessionId,
|
||||
req.body.contractId,
|
||||
req.jwt.unique_name,
|
||||
req.body.difficultyLevel!,
|
||||
req.gameVersion,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
res.send(`"${req.body.contractSessionId}"`)
|
||||
},
|
||||
)
|
||||
|
||||
async function loadSession(
|
||||
sessionId: string,
|
||||
token: string,
|
||||
sessionData?: ContractSession,
|
||||
): Promise<void> {
|
||||
if (!sessionData) {
|
||||
sessionData = await getContractSession(token + "_" + sessionId)
|
||||
}
|
||||
|
||||
contractSessions.set(sessionId, sessionData)
|
||||
log(
|
||||
LogLevel.DEBUG,
|
||||
`Loaded contract with token = ${token}, session id = ${sessionId}, start time = ${
|
||||
contractSessions.get(sessionId).timerStart
|
||||
}.`,
|
||||
)
|
||||
}
|
||||
|
||||
profileRouter.post(
|
||||
"/ProfileService/GetSemLinkStatus",
|
||||
jsonMiddleware(),
|
||||
|
@ -250,6 +250,32 @@ export interface ContractSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The SaveFile object passed by the client in /ProfileService/UpdateUserSaveFileTable
|
||||
*/
|
||||
export interface SaveFile {
|
||||
// The contract session ID of the save
|
||||
ContractSessionId: string
|
||||
// The unix timestamp at the time of saving
|
||||
TimeStamp: number
|
||||
Value: {
|
||||
// The name of the save slot
|
||||
Name: string
|
||||
// The token of the last event that happened before the save was made
|
||||
LastEventToken: string
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The body sent with the UpdateUserSaveFileTable request from the game after saving.
|
||||
*
|
||||
* @see SaveFile
|
||||
*/
|
||||
export interface UpdateUserSaveFileTableBody {
|
||||
clientSaveFileList: SaveFile[]
|
||||
deletedSaveFileList: SaveFile[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The Hitman server version in object form.
|
||||
*/
|
||||
@ -340,6 +366,13 @@ export interface UserProfile {
|
||||
}
|
||||
PeacockFavoriteContracts: string[]
|
||||
PeacockCompletedEscalations: string[]
|
||||
Saves: {
|
||||
[slot: string]: {
|
||||
Timestamp: number
|
||||
ContractSessionId: string
|
||||
Token: string
|
||||
}
|
||||
}
|
||||
ChallengeProgression: {
|
||||
[id: string]: ProfileChallengeData
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
"PeacockEscalations": {},
|
||||
"PeacockCompletedEscalations": [],
|
||||
"PeacockFavoriteContracts": [],
|
||||
"PeacockCChild": {},
|
||||
"Saves": {},
|
||||
"gameclient": {
|
||||
"LastConnectionVersion": {
|
||||
"_Major": 6,
|
||||
|
@ -5,7 +5,7 @@
|
||||
"PeacockEscalations": {},
|
||||
"PeacockFavoriteContracts": [],
|
||||
"PeacockCompletedEscalations": [],
|
||||
"PeacockCChild": {},
|
||||
"Saves": {},
|
||||
"gameclient": null,
|
||||
"progression": {
|
||||
"XPGain": 0,
|
||||
|
Loading…
x
Reference in New Issue
Block a user