diff --git a/packages/common/enums/osu.ts b/packages/common/enums/osu.ts index 5580aefd..6c7e6deb 100644 --- a/packages/common/enums/osu.ts +++ b/packages/common/enums/osu.ts @@ -35,6 +35,8 @@ export enum GameState { benchmark, tourney, charts + // spectating, + // watchingReplay } export enum LobbyStatus { diff --git a/packages/tosu/src/api/types/v2.ts b/packages/tosu/src/api/types/v2.ts index b4e8e5a7..173d4035 100644 --- a/packages/tosu/src/api/types/v2.ts +++ b/packages/tosu/src/api/types/v2.ts @@ -4,6 +4,7 @@ export type ApiAnswer = TosuAPi | { error?: string }; export type ApiAnswerPrecise = TosuPreciseAnswer | { error?: string }; export interface TosuAPi { + game: Game; client: string; server: string; state: NumberName; @@ -21,6 +22,11 @@ export interface TosuAPi { tourney: Tourney | undefined; } +export interface Game { + focused: boolean; + paused: boolean; +} + export interface BeatmapTime { live: number; firstObject: number; @@ -281,6 +287,8 @@ export interface Objects { } export interface Play { + failed: boolean; + playerName: string; mode: NumberName; score: number; diff --git a/packages/tosu/src/api/utils/buildResultV2.ts b/packages/tosu/src/api/utils/buildResultV2.ts index a1c3f371..6a9f4444 100644 --- a/packages/tosu/src/api/utils/buildResultV2.ts +++ b/packages/tosu/src/api/utils/buildResultV2.ts @@ -21,6 +21,7 @@ import path from 'path'; import { ApiAnswer, Leaderboard, + Play, Tourney, TourneyChatMessages, TourneyClients @@ -125,6 +126,10 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => { : menu.gamemode; return { + game: { + focused: instanceManager.gameFocused, + paused: global.paused + }, client: ClientType[osuInstance.client], server: osuInstance.customServerEndpoint ?? 'ppy.sh', state: { @@ -573,6 +578,7 @@ const buildLazerTourneyData = ( }, play: { + failed: false, playerName: client.score!.playerName, mode: { @@ -884,8 +890,9 @@ function buildPlay( gameplay: Gameplay, beatmapPP: BeatmapPP, currentMods: CalculateMods -) { +): Play { return { + failed: gameplay.failed, playerName: gameplay.playerName, mode: { diff --git a/packages/tosu/src/instances/lazerInstance.ts b/packages/tosu/src/instances/lazerInstance.ts index 4e52a0d9..333f0cfe 100644 --- a/packages/tosu/src/instances/lazerInstance.ts +++ b/packages/tosu/src/instances/lazerInstance.ts @@ -313,7 +313,7 @@ export class LazerInstance extends AbstractInstance { } break; - case GameState.play: // is playing (after player is loaded) + case GameState.play: // Reset gameplay data on retry if (this.previousTime > global.playTime) { gameplay.init(true); diff --git a/packages/tosu/src/instances/manager.ts b/packages/tosu/src/instances/manager.ts index d2be91c9..c4a784ad 100644 --- a/packages/tosu/src/instances/manager.ts +++ b/packages/tosu/src/instances/manager.ts @@ -21,6 +21,7 @@ import { OsuInstance } from './osuInstance'; export class InstanceManager { platformType: Platform; focusedClient: ClientType; + gameFocused: boolean; osuInstances: { [key: number]: AbstractInstance; @@ -179,6 +180,7 @@ export class InstanceManager { (r) => r.pid === focusedPID ); if (instance) this.focusedClient = instance.client; + this.gameFocused = instance !== undefined; if (this.focusedClient === undefined) { this.focusedClient = diff --git a/packages/tosu/src/memory/lazer.ts b/packages/tosu/src/memory/lazer.ts index 96a14f3a..d871d60d 100644 --- a/packages/tosu/src/memory/lazer.ts +++ b/packages/tosu/src/memory/lazer.ts @@ -32,6 +32,7 @@ import type { IMP3Length, IMenu, IResultScreen, + IScore, ISettings, ITourney, ITourneyChat, @@ -88,6 +89,8 @@ export interface Offsets { 'k__BackingField': number; 'k__BackingField': number; 'k__BackingField': number; + 'k__BackingField': number; + 'k__BackingField': number; beatmapClock: number; 'k__BackingField': number; 'k__BackingField': number; @@ -106,6 +109,11 @@ export interface Offsets { }; 'osu.Game.Screens.Play.PlayerLoader': { osuLogo: number; + 'k__BackingField': number; + }; + 'osu.Game.Screens.Play.SpectatorPlayer': { + score: number; + 'k__BackingField': number; }; 'osu.Game.Beatmaps.FramedBeatmapClock': { finalClockSource: number; @@ -160,6 +168,7 @@ export interface Offsets { 'k__BackingField': number; 'k__BackingField': number; 'k__BackingField': number; + 'k__BackingField': number; dependencies: number; }; 'osu.Framework.Screens.ScreenStack': { @@ -398,8 +407,6 @@ export class LazerMemory extends AbstractMemory { private HUDVisibilityMode: number = 0; private ReplaySettingsOverlay: boolean = true; - private replayMode: boolean = false; - private modMappings: Map = new Map(); private isPlayerLoading: boolean = false; @@ -645,9 +652,21 @@ export class LazerMemory extends AbstractMemory { 'k__BackingField' ] ) === + this.process.readIntPtr( + this.gameBase() + this.offsets['osu.Game.OsuGame'].osuLogo + ) && this.process.readIntPtr( - address + this.offsets['osu.Game.OsuGame'].osuLogo - ) + address + + this.offsets['osu.Game.Screens.Play.PlayerLoader'][ + 'k__BackingField' + ] + ) === + this.process.readIntPtr( + this.gameBase() + + this.offsets['osu.Game.OsuGameBase'][ + 'k__BackingField' + ] + ) ); } @@ -674,6 +693,69 @@ export class LazerMemory extends AbstractMemory { ); } + private checkIfSpectator(address: number) { + return ( + this.process.readIntPtr(address + 0x3f0) === + this.process.readIntPtr( + this.gameBase() + + this.offsets['osu.Game.OsuGameBase'][ + 'k__BackingField' + ] + ) && + this.process.readIntPtr( + address + + this.offsets['osu.Game.Screens.Play.SpectatorPlayer'][ + 'k__BackingField' + ] + ) === + this.process.readIntPtr( + this.gameBase() + + this.offsets['osu.Game.OsuGameBase'][ + 'k__BackingField' + ] + ) && + this.process.readIntPtr( + address + + this.offsets['osu.Game.Screens.Play.SpectatorPlayer'].score + ) !== + this.process.readIntPtr( + this.gameBase() + + this.offsets['osu.Game.OsuGameBase'][ + 'k__BackingField' + ] + ) + ); + } + + private checkIfWatchingReplay(address: number) { + const drawableRuleset = this.process.readIntPtr( + this.player() + + this.offsets['osu.Game.Screens.Play.Player'][ + 'k__BackingField' + ] + ); + return ( + this.process.readIntPtr( + drawableRuleset + + this.offsets['osu.Game.Rulesets.UI.DrawableRuleset'][ + 'k__BackingField' + ] + ) !== 0 && + this.process.readIntPtr( + address + + this.offsets['osu.Game.Screens.Play.SpectatorPlayer'][ + 'k__BackingField' + ] + ) !== + this.process.readIntPtr( + this.gameBase() + + this.offsets['osu.Game.OsuGameBase'][ + 'k__BackingField' + ] + ) + ); + } + private checkIfMultiSelect(address: number) { const multiplayerClient = this.multiplayerClient(); const isConnectedBindable = this.process.readIntPtr( @@ -1592,7 +1674,7 @@ export class LazerMemory extends AbstractMemory { health: number = 0, retries: number = 0, combo?: number - ): IGameplay { + ): IScore { const statistics = this.readStatistics(scoreInfo); const maximumStatistics = this.readMaximumStatistics(scoreInfo); @@ -1664,6 +1746,7 @@ export class LazerMemory extends AbstractMemory { } return { + failed: health <= 0, retries, playerName: username, mods, @@ -2870,18 +2953,29 @@ export class LazerMemory extends AbstractMemory { const filesFolder = path.join(this.basePath(), 'files'); const isPlaying = this.player() !== 0; + const isResultScreen = this.checkIfResultScreen(this.currentScreen); const isSongSelectV2 = this.checkIfSongSelectV2(this.currentScreen); const isPlayerLoader = this.checkIfPlayerLoader(this.currentScreen); const isEditor = this.checkIfEditor(this.currentScreen); + const isSpectator = this.checkIfSpectator(this.currentScreen); const isMultiSelect = this.checkIfMultiSelect(this.currentScreen); const isMulti = this.checkIfMulti(); + const isReplay = isPlaying + ? this.checkIfWatchingReplay(this.currentScreen) + : false; let isMultiSpectating = false; + let watchingReplay = false; let status = 0; - if (isPlaying || isPlayerLoader) { + if (isReplay && (isPlaying || isPlayerLoader)) { + watchingReplay = true; + status = GameState.play; + } else if (isSpectator && (isPlaying || isPlayerLoader)) { + status = GameState.play; + } else if (isPlaying || isPlayerLoader) { status = GameState.play; } else if (isSongSelectV2) { status = GameState.selectPlay; @@ -2912,26 +3006,8 @@ export class LazerMemory extends AbstractMemory { this.isPlayerLoading = isPlayerLoader; - if (isPlaying) { - const dependencies = this.process.readIntPtr( - this.player() + - this.offsets['osu.Game.Screens.Play.Player'].dependencies - ); - const cache = this.process.readIntPtr(dependencies + 0x8); - const entries = this.process.readIntPtr(cache + 0x10); - const drawableRuleset = this.process.readIntPtr(entries + 0x10); - - this.replayMode = - this.process.readIntPtr( - drawableRuleset + - this.offsets['osu.Game.Rulesets.UI.DrawableRuleset'][ - 'k__BackingField' - ] - ) !== 0; - } - return { - isWatchingReplay: this.replayMode, + isWatchingReplay: watchingReplay, isReplayUiHidden: !this.ReplaySettingsOverlay, showInterface: this.HUDVisibilityMode > 0, chatStatus: 0, diff --git a/packages/tosu/src/memory/stable.ts b/packages/tosu/src/memory/stable.ts index a4ad05cb..9c6d066e 100644 --- a/packages/tosu/src/memory/stable.ts +++ b/packages/tosu/src/memory/stable.ts @@ -587,6 +587,7 @@ export class StableMemory extends AbstractMemory { this.gameplayMode = mode; return { + failed: playerHP <= 0, retries, playerName, mods, @@ -790,11 +791,14 @@ export class StableMemory extends AbstractMemory { const rulesetAddr = this.process.readInt( this.process.readInt(rulesetsAddr - 0xb) + 0x4 ); + if (rulesetAddr !== 0) { isReplayUiHidden = Boolean( this.process.readByte(rulesetAddr + 0x1d8) ); } + + // status = GameState.watchingReplay; } const skinOsuAddr = this.process.readInt(skinDataAddr + 0x7); diff --git a/packages/tosu/src/memory/types.ts b/packages/tosu/src/memory/types.ts index a3919cc7..b32074fa 100644 --- a/packages/tosu/src/memory/types.ts +++ b/packages/tosu/src/memory/types.ts @@ -72,6 +72,7 @@ export type IResultScreen = | Error; export type IScore = { + failed: boolean; retries: number; playerName: string; mods: CalculateMods; diff --git a/packages/tosu/src/states/gameplay.ts b/packages/tosu/src/states/gameplay.ts index 1c73a3dc..cf4acdaa 100644 --- a/packages/tosu/src/states/gameplay.ts +++ b/packages/tosu/src/states/gameplay.ts @@ -52,6 +52,8 @@ export class Gameplay extends AbstractState { performanceAttributes: rosu.PerformanceAttributes | undefined; gradualPerformance: rosu.GradualPerformance | undefined; + failed: boolean; + retries: number; playerName: string; mods: CalculateMods = Object.assign({}, defaultCalculatedMods); @@ -99,6 +101,8 @@ export class Gameplay extends AbstractState { `gameplay init (${isRetry} - ${from})` ); + this.failed = false; + this.hitErrors = []; this.maxCombo = 0; this.score = 0; @@ -201,6 +205,8 @@ export class Gameplay extends AbstractState { // needed for ex like you done with replay watching/gameplay and return to mainMenu, you need alteast one reset to gameplay/resultScreen this.isDefaultState = false; + this.failed = result.failed; + this.retries = result.retries; this.playerName = result.playerName; this.mods = result.mods; diff --git a/packages/tosu/src/states/global.ts b/packages/tosu/src/states/global.ts index 6bb98462..84a0e622 100644 --- a/packages/tosu/src/states/global.ts +++ b/packages/tosu/src/states/global.ts @@ -14,8 +14,11 @@ export class Global extends AbstractState { chatStatus: number = 0; status: number = 0; + paused: boolean = false; gameTime: number = 0; playTime: number = 0; + previousPlayTime: number = 0; + menuMods: CalculateMods = Object.assign({}, defaultCalculatedMods); gameFolder: string = ''; @@ -64,6 +67,9 @@ export class Global extends AbstractState { this.gameTime = result.gameTime; this.menuMods = result.menuMods; + this.paused = this.previousPlayTime === this.playTime; + this.previousPlayTime = this.playTime; + this.skinFolder = safeJoin(result.skinFolder); this.memorySongsFolder = safeJoin(result.memorySongsFolder);