From b0ead1ac71196eff3ced4b797f73f6cd7a89971d Mon Sep 17 00:00:00 2001 From: urantij Date: Sat, 6 Jun 2026 17:05:05 +0700 Subject: [PATCH 1/5] add room to api --- packages/tosu/src/api/types/v2.ts | 3 +- packages/tosu/src/api/utils/buildResultV2.ts | 7 +- packages/tosu/src/instances/index.ts | 3 + packages/tosu/src/instances/lazerInstance.ts | 7 +- packages/tosu/src/memory/index.ts | 2 + packages/tosu/src/memory/lazer.ts | 94 ++++++++++++++++++++ packages/tosu/src/memory/stable.ts | 5 ++ packages/tosu/src/memory/types.ts | 17 ++++ packages/tosu/src/states/room.ts | 36 ++++++++ 9 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 packages/tosu/src/states/room.ts diff --git a/packages/tosu/src/api/types/v2.ts b/packages/tosu/src/api/types/v2.ts index 9f778ae5..7d1160c1 100644 --- a/packages/tosu/src/api/types/v2.ts +++ b/packages/tosu/src/api/types/v2.ts @@ -1,4 +1,4 @@ -import { IRankedPlay } from '@/memory/types'; +import { IRankedPlay, IRoom } from '@/memory/types'; import { CalculateMods } from '@/utils/osuMods.types'; export type ApiAnswer = TosuAPi | { error?: string }; @@ -22,6 +22,7 @@ export interface TosuAPi { directPath: DirectPath; tourney: Tourney | undefined; rankedPlay: IRankedPlay | undefined; + room: IRoom | undefined; } export interface Game { diff --git a/packages/tosu/src/api/utils/buildResultV2.ts b/packages/tosu/src/api/utils/buildResultV2.ts index 4a57fcc5..99ef763f 100644 --- a/packages/tosu/src/api/utils/buildResultV2.ts +++ b/packages/tosu/src/api/utils/buildResultV2.ts @@ -102,7 +102,8 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => { resultScreen, beatmapPP, user, - rankedPlay + rankedPlay, + room } = osuInstance.getServices([ 'settings', 'bassDensity', @@ -112,7 +113,8 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => { 'resultScreen', 'beatmapPP', 'user', - 'rankedPlay' + 'rankedPlay', + 'room' ]); const currentMods = @@ -145,6 +147,7 @@ export const buildResult = (instanceManager: InstanceManager): ApiAnswer => { playCount: 0 // need counting }, rankedPlay: rankedPlay.info, + room: room.info, settings: { interfaceVisible: global.showInterface, replayUIVisible: global.isReplayUiHidden === false, diff --git a/packages/tosu/src/instances/index.ts b/packages/tosu/src/instances/index.ts index b7ceca8b..37f55c77 100644 --- a/packages/tosu/src/instances/index.ts +++ b/packages/tosu/src/instances/index.ts @@ -16,6 +16,7 @@ import { LazerMultiSpectating } from '@/states/lazerMultiSpectating'; import { Menu } from '@/states/menu'; import { RankedPlay } from '@/states/rankedplay'; import { ResultScreen } from '@/states/resultScreen'; +import { Room } from '@/states/room'; import { Settings } from '@/states/settings'; import { TourneyManager } from '@/states/tourney'; import { User } from '@/states/user'; @@ -34,6 +35,7 @@ export interface DataRepoList { user: User; lazerMultiSpectating: LazerMultiSpectating; rankedPlay: RankedPlay; + room: Room; } export type OsuVersion = `b${number}${'tourney' | 'cuttingedge' | ''}`; @@ -89,6 +91,7 @@ export abstract class AbstractInstance { this.set('user', new User(this)); this.set('lazerMultiSpectating', new LazerMultiSpectating(this)); this.set('rankedPlay', new RankedPlay(this)); + this.set('room', new Room(this)); this.watchProcessHealth = this.watchProcessHealth.bind(this); this.preciseDataLoop = this.preciseDataLoop.bind(this); diff --git a/packages/tosu/src/instances/lazerInstance.ts b/packages/tosu/src/instances/lazerInstance.ts index d5cb8229..44e47b4d 100644 --- a/packages/tosu/src/instances/lazerInstance.ts +++ b/packages/tosu/src/instances/lazerInstance.ts @@ -166,7 +166,8 @@ export class LazerInstance extends AbstractInstance { settings, user, lazerMultiSpectating, - rankedPlay + rankedPlay, + room } = this.getServices([ 'global', 'menu', @@ -178,7 +179,8 @@ export class LazerInstance extends AbstractInstance { 'tourneyManager', 'user', 'lazerMultiSpectating', - 'rankedPlay' + 'rankedPlay', + 'room' ]); while (!this.isDestroyed) { @@ -212,6 +214,7 @@ export class LazerInstance extends AbstractInstance { } rankedPlay.updateState(); + room.updateState(); settings.updateState(); diff --git a/packages/tosu/src/memory/index.ts b/packages/tosu/src/memory/index.ts index d2a13c72..9f5ae668 100644 --- a/packages/tosu/src/memory/index.ts +++ b/packages/tosu/src/memory/index.ts @@ -14,6 +14,7 @@ import type { IMenu, IRankedPlay, IResultScreen, + IRoom, ISettings, ITourney, ITourneyChat, @@ -60,6 +61,7 @@ export abstract class AbstractMemory> { abstract tourneyUser(): ITourneyUser; abstract leaderboard(mode: number): ILeaderboard; abstract rankedPlay(): IRankedPlay; + abstract room(): IRoom; checkIsBasesValid(): boolean { Object.entries(this.patterns).map((entry) => diff --git a/packages/tosu/src/memory/lazer.ts b/packages/tosu/src/memory/lazer.ts index 60dfcf4a..4bc6e2f9 100644 --- a/packages/tosu/src/memory/lazer.ts +++ b/packages/tosu/src/memory/lazer.ts @@ -36,6 +36,7 @@ import type { IMenu, IRankedPlay, IResultScreen, + IRoom, IScore, ISettings, ITourney, @@ -267,6 +268,7 @@ export interface Offsets { 'osu.Game.Online.API.Requests.Responses.APIUser': { 'k__BackingField': number; 'k__BackingField': number; + AvatarUrl: number; countryCodeString: number; statistics: number; MatchmakingStatistics: number; @@ -2002,6 +2004,12 @@ export class LazerMemory extends AbstractMemory { 'osu.Game.Online.API.Requests.Responses.APIUser' ]['k__BackingField'] ), + avatarUrl: this.process.readSharpStringPtr( + user + + this.offsets[ + 'osu.Game.Online.API.Requests.Responses.APIUser' + ].AvatarUrl + ), accuracy, rankedScore, level, @@ -3770,6 +3778,92 @@ export class LazerMemory extends AbstractMemory { }; } + room(): IRoom { + const multiplayerClient = this.multiplayerClient(); + + if (!multiplayerClient) { + return 'not-ready'; + } + + const room = this.process.readIntPtr( + multiplayerClient + + this.offsets['osu.Game.Online.Multiplayer.MultiplayerClient'] + .room + ); + + if (!room) { + return 'not-ready'; + } + + const roomId = this.process.readLong( + room + + this.offsets['osu.Game.Online.Multiplayer.MultiplayerRoom'] + .RoomID + ); + + const multiplayerUsers = this.process.readIntPtr(room + 0x10); + const multiplayerUsersItems = this.process.readIntPtr( + multiplayerUsers + 0x8 + ); + const multiplayerUsersCount = this.process.readInt( + multiplayerUsers + 0x10 + ); + + const users: { + id: number; + info: + | { + username: string; + countryCode: CountryCodes; + avatarUrl: string | undefined; + } + | undefined; + }[] = []; + + for (let i = 0; i < multiplayerUsersCount; i++) { + const current = this.process.readIntPtr( + multiplayerUsersItems + 0x10 + 0x8 * i + ); + + const userId = this.process.readInt( + current + + this.offsets[ + 'osu.Game.Online.Multiplayer.MultiplayerRoomUser' + ].UserID + ); + + const apiUser = this.process.readInt( + current + + this.offsets[ + 'osu.Game.Online.Multiplayer.MultiplayerRoomUser' + ]['k__BackingField'] + ); + + if (apiUser) { + const user = this.readUser(apiUser); + + users.push({ + id: userId, + info: { + username: user.name, + countryCode: user.countryCode, + avatarUrl: user.avatarUrl + } + }); + } else { + users.push({ + id: userId, + info: undefined + }); + } + } + + return { + roomID: roomId, + users + }; + } + settings(): ISettings { const values = this.osuConfig(); values['client.version'] = (this.game as LazerInstance).version; diff --git a/packages/tosu/src/memory/stable.ts b/packages/tosu/src/memory/stable.ts index 8ebb4dc0..371babf6 100644 --- a/packages/tosu/src/memory/stable.ts +++ b/packages/tosu/src/memory/stable.ts @@ -24,6 +24,7 @@ import type { IOffsets, IRankedPlay, IResultScreen, + IRoom, ISettings, ITourney, ITourneyChat, @@ -1451,4 +1452,8 @@ export class StableMemory extends AbstractMemory { rankedPlay(): IRankedPlay | 'not-ready' { throw new Error('Stable does not have Ranked Play.'); } + + room(): IRoom { + return 'not-ready'; + } } diff --git a/packages/tosu/src/memory/types.ts b/packages/tosu/src/memory/types.ts index 2d3ea4d8..42cd8244 100644 --- a/packages/tosu/src/memory/types.ts +++ b/packages/tosu/src/memory/types.ts @@ -1,3 +1,4 @@ +import { CountryCodes } from '@tosu/common'; import type { RankedPlayStage } from '@tosu/common/enums/osu'; import type { ITourneyManagerChatItem } from '@/states/tourney'; @@ -231,4 +232,20 @@ export type IRankedPlay = } | string; +export type IRoom = + | { + roomID: number; + users: { + id: number; + info: + | { + username: string; + countryCode: CountryCodes; + avatarUrl: string | undefined; + } + | undefined; + }[]; + } + | string; + export type ISettings = SettingsObject | Error; diff --git a/packages/tosu/src/states/room.ts b/packages/tosu/src/states/room.ts new file mode 100644 index 00000000..f3371101 --- /dev/null +++ b/packages/tosu/src/states/room.ts @@ -0,0 +1,36 @@ +import { ClientType, measureTime, wLogger } from '@tosu/common'; + +import { IRoom } from '@/memory/types'; +import { AbstractState } from '@/states/index'; + +export class Room extends AbstractState { + info: IRoom | undefined = undefined; + + @measureTime + updateState() { + try { + const room = this.game.memory.room(); + if (room === 'not-ready') { + this.info = undefined; + } else { + this.info = room; + } + } catch (exc) { + this.info = undefined; + + this.game.reportError( + 'Room updatestate', + 10, + ClientType[this.game.client], + this.game.pid, + `Room updatestate`, + (exc as any).message + ); + wLogger.debug( + `%${ClientType[this.game.client]}%`, + `Error updating Room state:`, + exc + ); + } + } +} From 129a9e041c6f4a769b99982d7d102c5e2761d086 Mon Sep 17 00:00:00 2001 From: urantij Date: Sun, 7 Jun 2026 18:27:10 +0700 Subject: [PATCH 2/5] fixes room reading --- packages/tosu/src/memory/lazer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tosu/src/memory/lazer.ts b/packages/tosu/src/memory/lazer.ts index 4bc6e2f9..1d9bb2e7 100644 --- a/packages/tosu/src/memory/lazer.ts +++ b/packages/tosu/src/memory/lazer.ts @@ -3832,7 +3832,7 @@ export class LazerMemory extends AbstractMemory { ].UserID ); - const apiUser = this.process.readInt( + const apiUser = this.process.readIntPtr( current + this.offsets[ 'osu.Game.Online.Multiplayer.MultiplayerRoomUser' From ede97d1b258cb6aff4c485a64e11f70357e90e37 Mon Sep 17 00:00:00 2001 From: urantij Date: Sun, 5 Jul 2026 19:03:32 +0700 Subject: [PATCH 3/5] simplify room user model --- packages/tosu/src/assets/offsets.json | 2 +- packages/tosu/src/memory/lazer.ts | 26 ++++++-------------------- packages/tosu/src/memory/types.ts | 18 ++++++++---------- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/packages/tosu/src/assets/offsets.json b/packages/tosu/src/assets/offsets.json index 950774e7..0a2477e5 100644 --- a/packages/tosu/src/assets/offsets.json +++ b/packages/tosu/src/assets/offsets.json @@ -1 +1 @@ -{"OsuVersion":"2026.624.0","osu.Game.OsuGame":{"osuLogo":1584,"k__BackingField":1544,"k__BackingField":1480,"channelManager":1320,"sentryLogger":1512,"chatOverlay":1312},"osu.Framework.Game":{"k__BackingField":824},"osu.Game.OsuGameBase":{"k__BackingField":1080,"k__BackingField":1192,"k__BackingField":1024,"k__BackingField":1200,"k__BackingField":1168,"k__BackingField":992,"beatmapClock":1232,"k__BackingField":1088,"k__BackingField":1104,"k__BackingField":1040,"k__BackingField":976,"k__BackingField":984,"rulesetConfigCache":1176,"realm":1216},"osu.Game.Screens.Select.SongSelect":{"k__BackingField":1032},"osu.Game.Screens.Play.SubmittingPlayer":{"k__BackingField":1264,"k__BackingField":1272},"osu.Game.Screens.Play.PlayerLoader":{"osuLogo":1176,"k__BackingField":1168},"osu.Game.Screens.Play.SpectatorPlayer":{"score":1272,"k__BackingField":1264},"osu.Game.Beatmaps.FramedBeatmapClock":{"finalClockSource":528,"decoupledTrack":552},"osu.Framework.Timing.DecouplingFramedClock":{"k__BackingField":24},"osu.Framework.Audio.Track.Track":{"length":72},"osu.Framework.Timing.FramedClock":{"k__BackingField":48},"osu.Game.Screens.OsuScreen":{"k__BackingField":896},"osu.Game.Screens.Ranking.SoloResultsScreen":{"k__BackingField":1040,"SelectedScore":920},"osu.Game.Screens.Edit.Editor":{"k__BackingField":960,"k__BackingField":1096},"osu.Game.Screens.OnlinePlay.OnlinePlayScreen":{"k__BackingField":960},"osu.Game.Screens.OnlinePlay.Multiplayer.Multiplayer":{"k__BackingField":976},"osu.Game.Online.Multiplayer.MultiplayerRoom":{"RoomID":56,"k__BackingField":68,"k__BackingField":32},"osu.Game.Screens.Spectate.SpectatorScreen":{"k__BackingField":944,"gameplayStates":992},"osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.MultiSpectatorScreen":{"k__BackingField":1032},"osu.Game.Online.Multiplayer.OnlineMultiplayerClient":{"k__BackingField":904},"osu.Game.Online.Multiplayer.MultiplayerClient":{"room":800},"osu.Game.Overlays.ChatOverlay":{"State":808},"osu.Game.Screens.Menu.OsuLogo":{"visualizer":928},"osu.Game.Screens.Menu.LogoVisualisation":{"frequencyAmplitudes":504},"osu.Game.Screens.Play.Player":{"k__BackingField":1008,"k__BackingField":1000,"k__BackingField":1152,"k__BackingField":1096,"k__BackingField":1104,"k__BackingField":1120,"k__BackingField":1112,"dependencies":1168},"osu.Framework.Screens.ScreenStack":{"stack":800},"osu.Game.Rulesets.RulesetConfigCache":{"configCache":520},"osu.Game.Online.Chat.ExternalLinkOpener":{"k__BackingField":536},"osu.Game.Online.API.APIAccess":{"localUserState":864,"game":784},"osu.Game.Online.API.LocalUserState":{"localUser":512},"osu.Desktop.OsuGameDesktop":{"AvailableMods":1128,"SelectedMods":1120,"Ruleset":1112},"osu.Game.Scoring.ScoreInfo":{"k__BackingField":184,"k__BackingField":192,"k__BackingField":80,"k__BackingField":120,"k__BackingField":72,"k__BackingField":160,"k__BackingField":204,"k__BackingField":212,"k__BackingField":220,"k__BackingField":48,"k__BackingField":176,"k__BackingField":256,"statistics":128,"maximumStatistics":136,"user":112},"osu.Framework.Platform.Storage":{"k__BackingField":8},"osu.Game.IO.WrappedStorage":{"k__BackingField":16},"osu.Game.Beatmaps.WorkingBeatmapCache+BeatmapManagerWorkingBeatmap":{"BeatmapInfo":8,"BeatmapSetInfo":16},"osu.Game.Models.RealmUser":{"k__BackingField":24,"k__BackingField":40},"osu.Game.Rulesets.RulesetInfo":{"k__BackingField":48},"osu.Game.Screens.Play.SoloPlayer":{"RestartCount":916},"osu.Game.Rulesets.Osu.Scoring.OsuScoreProcessor":{"Combo":592},"osu.Game.Rulesets.Scoring.ScoreProcessor":{"hitEvents":656},"osu.Game.Online.API.Requests.Responses.APIMe":{"PlayMode":136},"osu.Game.Online.API.Requests.Responses.APIUser":{"k__BackingField":248,"k__BackingField":8,"countryCodeString":32,"statistics":160,"MatchmakingStatistics":240},"osu.Game.Online.API.Requests.Responses.APIUserMatchmakingStatistics":{"k__BackingField":24,"k__BackingField":44,"k__BackingField":28,"k__BackingField":36,"k__BackingField":40},"osu.Game.Online.Chat.ChannelManager":{"joinedChannels":800},"osu.Game.Online.Chat.Channel":{"Id":96,"Messages":16},"osu.Game.Online.Chat.Message":{"Timestamp":88,"Content":8,"Sender":16},"osu.Game.Users.UserStatistics":{"RankedScore":32,"GlobalRank":84,"PlayCount":56,"Accuracy":40,"Level":76,"PP":112},"osu.Game.Rulesets.Osu.Scoring.OsuHealthProcessor":{"Health":560},"osu.Game.Screens.Play.HUDOverlay":{"InputCountController":832,"k__BackingField":880},"osu.Game.Screens.Play.HUD.InputCountController":{"triggers":512},"osu.Game.Rulesets.UI.DrawableRuleset":{"k__BackingField":808},"osu.Game.Beatmaps.BeatmapInfo":{"k__BackingField":140,"k__BackingField":88,"k__BackingField":136,"k__BackingField":48,"k__BackingField":40,"k__BackingField":24,"k__BackingField":148,"k__BackingField":80},"osu.Game.Beatmaps.BeatmapSetInfo":{"k__BackingField":48},"osu.Game.Beatmaps.BeatmapMetadata":{"k__BackingField":24,"<TitleUnicode>k__BackingField":32,"<Artist>k__BackingField":40,"<ArtistUnicode>k__BackingField":48,"<Author>k__BackingField":56,"<Source>k__BackingField":64,"<Tags>k__BackingField":72,"<UserTags>k__BackingField":80,"<PreviewTime>k__BackingField":104,"<AudioFile>k__BackingField":88,"<BackgroundFile>k__BackingField":96},"osu.Game.Beatmaps.BeatmapDifficulty":{"<DrainRate>k__BackingField":40,"<CircleSize>k__BackingField":44,"<OverallDifficulty>k__BackingField":48,"<ApproachRate>k__BackingField":52,"<SliderMultiplier>k__BackingField":24,"<SliderTickRate>k__BackingField":32},"osu.Game.Online.Multiplayer.MultiplayerRoomUser":{"UserID":40,"<State>k__BackingField":44,"<BeatmapAvailability>k__BackingField":8,"<Mods>k__BackingField":16,"<MatchState>k__BackingField":24,"RulesetId":56,"BeatmapId":64,"<User>k__BackingField":32},"osu.Game.Online.Multiplayer.MatchTypes.RankedPlay.RankedPlayRoomState":{"<Stage>k__BackingField":32,"<CurrentRound>k__BackingField":36,"<DamageMultiplier>k__BackingField":16,"<Users>k__BackingField":8,"<ActiveUserId>k__BackingField":40,"<StarRating>k__BackingField":24,"<WinningUserId>k__BackingField":48},"osu.Game.Online.Multiplayer.MatchTypes.RankedPlay.RankedPlayUserInfo":{"<Rating>k__BackingField":32,"<Life>k__BackingField":36,"<Hand>k__BackingField":8,"<RatingAfter>k__BackingField":40,"DamageInfo":16,"<RoundsWon>k__BackingField":44,"<DamageMultiplier>k__BackingField":24},"osu.Game.Skinning.SkinManager":{"CurrentSkin":80},"osu.Game.Skinning.Skin":{"<Name>k__BackingField":64},"osu.Framework.Platform.GameHost":{"<inputConfig>k__BackingField":48},"osu.Framework.Configuration.InputConfigManager":{"<InputHandlers>k__BackingField":32},"osu.Framework.Input.Handlers.Tablet.OpenTabletDriverHandler":{"<AreaOffset>k__BackingField":72,"<AreaSize>k__BackingField":80,"<Rotation>k__BackingField":104,"<PressureThreshold>k__BackingField":112},"osu.Framework.Input.Handlers.Mouse.MouseHandler":{"<UseRelativeMode>k__BackingField":40},"osu.Game.Utils.SentryLogger":{"sentrySession":16},"Sentry.SentrySdk+DisposeHandle":{"_localHub":8},"Sentry.Internal.Hub":{"_options":32},"Sentry.SentryOptions":{"<Release>k__BackingField":176},"GameBaseVtable":7765317648384} \ No newline at end of file +{"OsuVersion":"2026.624.0","osu.Game.OsuGame":{"osuLogo":1584,"<ScreenStack>k__BackingField":1544,"<frameworkConfig>k__BackingField":1480,"channelManager":1320,"sentryLogger":1512,"chatOverlay":1312},"osu.Framework.Game":{"<Host>k__BackingField":824},"osu.Game.OsuGameBase":{"<API>k__BackingField":1080,"<SpectatorClient>k__BackingField":1192,"<ScoreManager>k__BackingField":1024,"<MultiplayerClient>k__BackingField":1200,"<LeaderboardManager>k__BackingField":1168,"<SessionStatics>k__BackingField":992,"beatmapClock":1232,"<Storage>k__BackingField":1088,"<Beatmap>k__BackingField":1104,"<SkinManager>k__BackingField":1040,"<VersionHash>k__BackingField":976,"<LocalConfig>k__BackingField":984,"rulesetConfigCache":1176,"realm":1216},"osu.Game.Screens.Select.SongSelect":{"<game>k__BackingField":1032},"osu.Game.Screens.Play.SubmittingPlayer":{"<api>k__BackingField":1264,"<spectatorClient>k__BackingField":1272},"osu.Game.Screens.Play.PlayerLoader":{"osuLogo":1176,"<leaderboardManager>k__BackingField":1168},"osu.Game.Screens.Play.SpectatorPlayer":{"score":1272,"<SpectatorClient>k__BackingField":1264},"osu.Game.Beatmaps.FramedBeatmapClock":{"finalClockSource":528,"decoupledTrack":552},"osu.Framework.Timing.DecouplingFramedClock":{"<Source>k__BackingField":24},"osu.Framework.Audio.Track.Track":{"length":72},"osu.Framework.Timing.FramedClock":{"<CurrentTime>k__BackingField":48},"osu.Game.Screens.OsuScreen":{"<logo>k__BackingField":896},"osu.Game.Screens.Ranking.SoloResultsScreen":{"<api>k__BackingField":1040,"SelectedScore":920},"osu.Game.Screens.Edit.Editor":{"<realm>k__BackingField":960,"<api>k__BackingField":1096},"osu.Game.Screens.OnlinePlay.OnlinePlayScreen":{"<API>k__BackingField":960},"osu.Game.Screens.OnlinePlay.Multiplayer.Multiplayer":{"<client>k__BackingField":976},"osu.Game.Online.Multiplayer.MultiplayerRoom":{"RoomID":56,"<ChannelID>k__BackingField":68,"<MatchState>k__BackingField":32},"osu.Game.Screens.Spectate.SpectatorScreen":{"<spectatorClient>k__BackingField":944,"gameplayStates":992},"osu.Game.Screens.OnlinePlay.Multiplayer.Spectate.MultiSpectatorScreen":{"<multiplayerClient>k__BackingField":1032},"osu.Game.Online.Multiplayer.OnlineMultiplayerClient":{"<IsConnected>k__BackingField":904},"osu.Game.Online.Multiplayer.MultiplayerClient":{"room":800},"osu.Game.Overlays.ChatOverlay":{"State":808},"osu.Game.Screens.Menu.OsuLogo":{"visualizer":928},"osu.Game.Screens.Menu.LogoVisualisation":{"frequencyAmplitudes":504},"osu.Game.Screens.Play.Player":{"<api>k__BackingField":1008,"<scoreManager>k__BackingField":1000,"<Score>k__BackingField":1152,"<ScoreProcessor>k__BackingField":1096,"<HealthProcessor>k__BackingField":1104,"<HUDOverlay>k__BackingField":1120,"<DrawableRuleset>k__BackingField":1112,"dependencies":1168},"osu.Framework.Screens.ScreenStack":{"stack":800},"osu.Game.Rulesets.RulesetConfigCache":{"configCache":520},"osu.Game.Online.Chat.ExternalLinkOpener":{"<api>k__BackingField":536},"osu.Game.Online.API.APIAccess":{"localUserState":864,"game":784},"osu.Game.Online.API.LocalUserState":{"localUser":512},"osu.Desktop.OsuGameDesktop":{"AvailableMods":1128,"SelectedMods":1120,"Ruleset":1112},"osu.Game.Scoring.ScoreInfo":{"<OnlineID>k__BackingField":184,"<LegacyOnlineID>k__BackingField":192,"<ModsJson>k__BackingField":80,"<HitEvents>k__BackingField":120,"<RealmUser>k__BackingField":72,"<TotalScore>k__BackingField":160,"<MaxCombo>k__BackingField":204,"<Combo>k__BackingField":212,"<Passed>k__BackingField":220,"<Ruleset>k__BackingField":48,"<Accuracy>k__BackingField":176,"<Date>k__BackingField":256,"statistics":128,"maximumStatistics":136,"user":112},"osu.Framework.Platform.Storage":{"<BasePath>k__BackingField":8},"osu.Game.IO.WrappedStorage":{"<UnderlyingStorage>k__BackingField":16},"osu.Game.Beatmaps.WorkingBeatmapCache+BeatmapManagerWorkingBeatmap":{"BeatmapInfo":8,"BeatmapSetInfo":16},"osu.Game.Models.RealmUser":{"<Username>k__BackingField":24,"<OnlineID>k__BackingField":40},"osu.Game.Rulesets.RulesetInfo":{"<OnlineID>k__BackingField":48},"osu.Game.Screens.Play.SoloPlayer":{"RestartCount":916},"osu.Game.Rulesets.Osu.Scoring.OsuScoreProcessor":{"Combo":592},"osu.Game.Rulesets.Scoring.ScoreProcessor":{"hitEvents":656},"osu.Game.Online.API.Requests.Responses.APIMe":{"PlayMode":136},"osu.Game.Online.API.Requests.Responses.APIUser":{"<Id>k__BackingField":248,"<Username>k__BackingField":8,"AvatarUrl":56,"countryCodeString":32,"statistics":160,"MatchmakingStatistics":240},"osu.Game.Online.API.Requests.Responses.APIUserMatchmakingStatistics":{"<Rating>k__BackingField":24,"<Rank>k__BackingField":44,"<Plays>k__BackingField":28,"<FirstPlacements>k__BackingField":36,"<IsRatingProvisional>k__BackingField":40},"osu.Game.Online.Chat.ChannelManager":{"joinedChannels":800},"osu.Game.Online.Chat.Channel":{"Id":96,"Messages":16},"osu.Game.Online.Chat.Message":{"Timestamp":88,"Content":8,"Sender":16},"osu.Game.Users.UserStatistics":{"RankedScore":32,"GlobalRank":84,"PlayCount":56,"Accuracy":40,"Level":76,"PP":112},"osu.Game.Rulesets.Osu.Scoring.OsuHealthProcessor":{"Health":560},"osu.Game.Screens.Play.HUDOverlay":{"InputCountController":832,"<ShowHud>k__BackingField":880},"osu.Game.Screens.Play.HUD.InputCountController":{"triggers":512},"osu.Game.Rulesets.UI.DrawableRuleset":{"<ReplayScore>k__BackingField":808},"osu.Game.Beatmaps.BeatmapInfo":{"<OnlineID>k__BackingField":140,"<MD5Hash>k__BackingField":88,"<StatusInt>k__BackingField":136,"<Metadata>k__BackingField":48,"<Difficulty>k__BackingField":40,"<DifficultyName>k__BackingField":24,"<TotalObjectCount>k__BackingField":148,"<Hash>k__BackingField":80},"osu.Game.Beatmaps.BeatmapSetInfo":{"<OnlineID>k__BackingField":48},"osu.Game.Beatmaps.BeatmapMetadata":{"<Title>k__BackingField":24,"<TitleUnicode>k__BackingField":32,"<Artist>k__BackingField":40,"<ArtistUnicode>k__BackingField":48,"<Author>k__BackingField":56,"<Source>k__BackingField":64,"<Tags>k__BackingField":72,"<UserTags>k__BackingField":80,"<PreviewTime>k__BackingField":104,"<AudioFile>k__BackingField":88,"<BackgroundFile>k__BackingField":96},"osu.Game.Beatmaps.BeatmapDifficulty":{"<DrainRate>k__BackingField":40,"<CircleSize>k__BackingField":44,"<OverallDifficulty>k__BackingField":48,"<ApproachRate>k__BackingField":52,"<SliderMultiplier>k__BackingField":24,"<SliderTickRate>k__BackingField":32},"osu.Game.Online.Multiplayer.MultiplayerRoomUser":{"UserID":40,"<State>k__BackingField":44,"<BeatmapAvailability>k__BackingField":8,"<Mods>k__BackingField":16,"<MatchState>k__BackingField":24,"RulesetId":56,"BeatmapId":64,"<User>k__BackingField":32},"osu.Game.Online.Multiplayer.MatchTypes.RankedPlay.RankedPlayRoomState":{"<Stage>k__BackingField":32,"<CurrentRound>k__BackingField":36,"<DamageMultiplier>k__BackingField":16,"<Users>k__BackingField":8,"<ActiveUserId>k__BackingField":40,"<StarRating>k__BackingField":24,"<WinningUserId>k__BackingField":48},"osu.Game.Online.Multiplayer.MatchTypes.RankedPlay.RankedPlayUserInfo":{"<Rating>k__BackingField":32,"<Life>k__BackingField":36,"<Hand>k__BackingField":8,"<RatingAfter>k__BackingField":40,"DamageInfo":16,"<RoundsWon>k__BackingField":44,"<DamageMultiplier>k__BackingField":24},"osu.Game.Skinning.SkinManager":{"CurrentSkin":80},"osu.Game.Skinning.Skin":{"<Name>k__BackingField":64},"osu.Framework.Platform.GameHost":{"<inputConfig>k__BackingField":48},"osu.Framework.Configuration.InputConfigManager":{"<InputHandlers>k__BackingField":32},"osu.Framework.Input.Handlers.Tablet.OpenTabletDriverHandler":{"<AreaOffset>k__BackingField":72,"<AreaSize>k__BackingField":80,"<Rotation>k__BackingField":104,"<PressureThreshold>k__BackingField":112},"osu.Framework.Input.Handlers.Mouse.MouseHandler":{"<UseRelativeMode>k__BackingField":40},"osu.Game.Utils.SentryLogger":{"sentrySession":16},"Sentry.SentrySdk+DisposeHandle":{"_localHub":8},"Sentry.Internal.Hub":{"_options":32},"Sentry.SentryOptions":{"<Release>k__BackingField":176},"GameBaseVtable":7765317648384} \ No newline at end of file diff --git a/packages/tosu/src/memory/lazer.ts b/packages/tosu/src/memory/lazer.ts index 1d9bb2e7..abe265e3 100644 --- a/packages/tosu/src/memory/lazer.ts +++ b/packages/tosu/src/memory/lazer.ts @@ -37,6 +37,7 @@ import type { IRankedPlay, IResultScreen, IRoom, + IRoomUser, IScore, ISettings, ITourney, @@ -3809,16 +3810,7 @@ export class LazerMemory extends AbstractMemory<LazerPatternData> { multiplayerUsers + 0x10 ); - const users: { - id: number; - info: - | { - username: string; - countryCode: CountryCodes; - avatarUrl: string | undefined; - } - | undefined; - }[] = []; + const users: IRoomUser[] = []; for (let i = 0; i < multiplayerUsersCount; i++) { const current = this.process.readIntPtr( @@ -3839,21 +3831,15 @@ export class LazerMemory extends AbstractMemory<LazerPatternData> { ]['<User>k__BackingField'] ); + // during my tests, this field was never null. but just in case, lets skip a user if its null if (apiUser) { const user = this.readUser(apiUser); users.push({ id: userId, - info: { - username: user.name, - countryCode: user.countryCode, - avatarUrl: user.avatarUrl - } - }); - } else { - users.push({ - id: userId, - info: undefined + username: user.name, + countryCode: user.countryCode, + avatarUrl: user.avatarUrl }); } } diff --git a/packages/tosu/src/memory/types.ts b/packages/tosu/src/memory/types.ts index 42cd8244..5c529418 100644 --- a/packages/tosu/src/memory/types.ts +++ b/packages/tosu/src/memory/types.ts @@ -232,19 +232,17 @@ export type IRankedPlay = } | string; +export type IRoomUser = { + id: number; + username: string; + countryCode: CountryCodes; + avatarUrl: string | undefined; +}; + export type IRoom = | { roomID: number; - users: { - id: number; - info: - | { - username: string; - countryCode: CountryCodes; - avatarUrl: string | undefined; - } - | undefined; - }[]; + users: IRoomUser[]; } | string; From ac5e7678cb6db5b97e98055aa87a647bf72bfc60 Mon Sep 17 00:00:00 2001 From: urantij <urantij@gmail.com> Date: Sun, 5 Jul 2026 19:13:01 +0700 Subject: [PATCH 4/5] simplify room assigment --- packages/tosu/src/states/room.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/tosu/src/states/room.ts b/packages/tosu/src/states/room.ts index f3371101..51cdc7cc 100644 --- a/packages/tosu/src/states/room.ts +++ b/packages/tosu/src/states/room.ts @@ -10,11 +10,7 @@ export class Room extends AbstractState { updateState() { try { const room = this.game.memory.room(); - if (room === 'not-ready') { - this.info = undefined; - } else { - this.info = room; - } + this.info = room !== 'not-ready' ? room : undefined; } catch (exc) { this.info = undefined; From 2e54bada70fb39582d52e840c11f2e5c268c9b2c Mon Sep 17 00:00:00 2001 From: urantij <urantij@gmail.com> Date: Sat, 11 Jul 2026 09:45:21 +0700 Subject: [PATCH 5/5] change to type imports --- packages/tosu/src/api/types/v2.ts | 4 ++-- packages/tosu/src/states/room.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/tosu/src/api/types/v2.ts b/packages/tosu/src/api/types/v2.ts index 7d1160c1..40a50e05 100644 --- a/packages/tosu/src/api/types/v2.ts +++ b/packages/tosu/src/api/types/v2.ts @@ -1,5 +1,5 @@ -import { IRankedPlay, IRoom } from '@/memory/types'; -import { CalculateMods } from '@/utils/osuMods.types'; +import { type IRankedPlay, type IRoom } from '@/memory/types'; +import { type CalculateMods } from '@/utils/osuMods.types'; export type ApiAnswer = TosuAPi | { error?: string }; export type ApiAnswerPrecise = TosuPreciseAnswer | { error?: string }; diff --git a/packages/tosu/src/states/room.ts b/packages/tosu/src/states/room.ts index 51cdc7cc..b17f0af6 100644 --- a/packages/tosu/src/states/room.ts +++ b/packages/tosu/src/states/room.ts @@ -1,6 +1,6 @@ import { ClientType, measureTime, wLogger } from '@tosu/common'; -import { IRoom } from '@/memory/types'; +import { type IRoom } from '@/memory/types'; import { AbstractState } from '@/states/index'; export class Room extends AbstractState {