Skip to content

Commit

Permalink
Merge pull request #10899 from nanaya/legacy-display
Browse files Browse the repository at this point in the history
Show correct legacy attributes on non-lazer mode
  • Loading branch information
peppy authored Jan 29, 2024
2 parents 87f5970 + 87e4740 commit c696378
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 16 deletions.
4 changes: 2 additions & 2 deletions resources/js/beatmapsets-show/scoreboard/table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import PpValue from 'scores/pp-value';
import { classWithModifiers, Modifiers } from 'utils/css';
import { formatNumber } from 'utils/html';
import { trans } from 'utils/lang';
import { hasMenu, isPerfectCombo, modeAttributesMap, scoreUrl, totalScore } from 'utils/score-helper';
import { filterMods, hasMenu, isPerfectCombo, modeAttributesMap, scoreUrl, totalScore } from 'utils/score-helper';

const bn = 'beatmap-scoreboard-table';

Expand Down Expand Up @@ -149,7 +149,7 @@ export default class ScoreboardTableRow extends React.Component<Props> {

<TdLink href={this.scoreUrl} modifiers='mods'>
<div className={`${bn}__mods`}>
{score.mods.map((mod) => <Mod key={mod.acronym} mod={mod} />)}
{filterMods(score).map((mod) => <Mod key={mod.acronym} mod={mod} />)}
</div>
</TdLink>

Expand Down
4 changes: 2 additions & 2 deletions resources/js/beatmapsets-show/scoreboard/top-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { rulesetName, shouldShowPp } from 'utils/beatmap-helper';
import { classWithModifiers, Modifiers } from 'utils/css';
import { formatNumber } from 'utils/html';
import { trans } from 'utils/lang';
import { isPerfectCombo, modeAttributesMap, scoreUrl, totalScore } from 'utils/score-helper';
import { filterMods, isPerfectCombo, modeAttributesMap, scoreUrl, totalScore } from 'utils/score-helper';

interface Props {
beatmap: BeatmapJson;
Expand Down Expand Up @@ -182,7 +182,7 @@ export default class TopCard extends React.PureComponent<Props> {
{trans('beatmapsets.show.scoreboard.headers.mods')}
</div>
<div className='beatmap-score-top__stat-value beatmap-score-top__stat-value--mods u-hover'>
{this.props.score.mods.map((mod) => <Mod key={mod.acronym} mod={mod} />)}
{filterMods(this.props.score).map((mod) => <Mod key={mod.acronym} mod={mod} />)}
</div>
</div>
</div>
Expand Down
2 changes: 2 additions & 0 deletions resources/js/interfaces/user-preferences-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const defaultUserPreferencesJson: UserPreferencesJson = {
comments_show_deleted: false,
comments_sort: 'new',
forum_posts_show_deleted: true,
legacy_score_only: true,
profile_cover_expanded: true,
user_list_filter: 'all',
user_list_sort: 'last_visit',
Expand All @@ -33,6 +34,7 @@ export default interface UserPreferencesJson {
comments_show_deleted: boolean;
comments_sort: string;
forum_posts_show_deleted: boolean;
legacy_score_only: boolean;
profile_cover_expanded: boolean;
user_list_filter: Filter;
user_list_sort: SortMode;
Expand Down
1 change: 1 addition & 0 deletions resources/js/mp-history/game-header.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import * as React from 'react'
import { div, a, span, h1, h2 } from 'react-dom-factories'
import { getArtist, getTitle } from 'utils/beatmapset-helper'
import { trans } from 'utils/lang'
import { filterMods } from 'utils/score-helper'

el = React.createElement

Expand Down
11 changes: 6 additions & 5 deletions resources/js/profile-page/play-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { getArtist, getTitle } from 'utils/beatmapset-helper';
import { classWithModifiers } from 'utils/css';
import { formatNumber } from 'utils/html';
import { trans } from 'utils/lang';
import { hasMenu } from 'utils/score-helper';
import { accuracy, filterMods, hasMenu, rank } from 'utils/score-helper';
import { beatmapUrl } from 'utils/url';

const bn = 'play-detail';
Expand Down Expand Up @@ -52,13 +52,14 @@ export default class PlayDetail extends React.PureComponent<Props, State> {
}

const scoreWeight = this.props.showPpWeight ? score.weight : null;
const scoreRank = rank(score);

return (
<div className={blockClass} {...additionalAttributes}>
{this.renderPinSortableHandle()}
<div className={`${bn}__group ${bn}__group--top`}>
<div className={`${bn}__icon ${bn}__icon--main`}>
<div className={`score-rank score-rank--full score-rank--${score.rank}`} />
<div className={`score-rank score-rank--full score-rank--${scoreRank}`} />
</div>

<div className={`${bn}__detail`}>
Expand Down Expand Up @@ -86,12 +87,12 @@ export default class PlayDetail extends React.PureComponent<Props, State> {
<div className={`${bn}__group ${bn}__group--bottom`}>
<div className={`${bn}__score-detail ${bn}__score-detail--score`}>
<div className={`${bn}__icon ${bn}__icon--extra`}>
<div className={`score-rank score-rank--full score-rank--${score.rank}`} />
<div className={`score-rank score-rank--full score-rank--${scoreRank}`} />
</div>
<div className={`${bn}__score-detail-top-right`}>
<div className={`${bn}__accuracy-and-weighted-pp`}>
<span className={`${bn}__accuracy`}>
{formatNumber(score.accuracy * 100, 2)}%
{formatNumber(accuracy(score) * 100, 2)}%
</span>
{scoreWeight != null && (
<span className={`${bn}__weighted-pp`}>
Expand All @@ -111,7 +112,7 @@ export default class PlayDetail extends React.PureComponent<Props, State> {
</div>

<div className={`${bn}__score-detail ${bn}__score-detail--mods`}>
{score.mods.map((mod) => <Mod key={mod.acronym} mod={mod} />)}
{filterMods(score).map((mod) => <Mod key={mod.acronym} mod={mod} />)}
</div>

<div className={`${bn}__pp`}>
Expand Down
5 changes: 3 additions & 2 deletions resources/js/scores-show/info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import BeatmapsetCover from 'components/beatmapset-cover';
import { SoloScoreJsonForShow } from 'interfaces/solo-score-json';
import * as React from 'react';
import { rulesetName } from 'utils/beatmap-helper';
import { accuracy, rank } from 'utils/score-helper';
import Buttons from './buttons';
import Dial from './dial';
import Player from './player';
Expand All @@ -22,11 +23,11 @@ export default function Info({ score }: Props) {
</div>

<div className='score-info__item'>
<Tower rank={score.rank} />
<Tower rank={rank(score)} />
</div>

<div className='score-info__item score-info__item--dial'>
<Dial accuracy={score.accuracy} mode={rulesetName(score.ruleset_id)} rank={score.rank} />
<Dial accuracy={accuracy(score)} mode={rulesetName(score.ruleset_id)} rank={rank(score)} />
</div>

<div className='score-info__item score-info__item--player'>
Expand Down
4 changes: 2 additions & 2 deletions resources/js/scores-show/player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as moment from 'moment';
import * as React from 'react';
import { formatNumber } from 'utils/html';
import { trans } from 'utils/lang';
import { totalScore } from 'utils/score-helper';
import { filterMods, totalScore } from 'utils/score-helper';

interface Props {
score: SoloScoreJsonForShow;
Expand All @@ -22,7 +22,7 @@ export default function Player(props: Props) {
</div>

<div className='score-player__mods'>
{props.score.mods.map((mod) => (
{filterMods(props.score).map((mod) => (
<div key={mod.acronym} className='score-player__mod'>
<Mod mod={mod} />
</div>
Expand Down
158 changes: 158 additions & 0 deletions resources/js/utils/legacy-score-helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the GNU Affero General Public License v3.0.
// See the LICENCE file in the repository root for full licence text.

import Rank from 'interfaces/rank';
import SoloScoreJson from 'interfaces/solo-score-json';

interface CacheEntry {
accuracy: number;
rank: Rank;
}
let cache: Partial<Record<string, CacheEntry>> = {};

// reset cache on navigation
document.addEventListener('turbolinks:load', () => {
cache = {};
});

function shouldHaveHiddenRank(score: SoloScoreJson) {
return score.mods.some((mod) => mod.acronym === 'FL' || mod.acronym === 'HD');
}

export function legacyAccuracyAndRank(score: SoloScoreJson) {
const key = `${score.type}:${score.id}`;
let cached = cache[key];

if (cached == null) {
const countMiss = score.statistics.miss ?? 0;
const countGreat = score.statistics.great ?? 0;

let accuracy: number;
let rank: Rank;

// Reference: https://github.com/ppy/osu/blob/e3ffea1b127cbd3171010972588a8b07cf049ba0/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs#L170-L274
switch (score.ruleset_id) {
// osu
case 0: {
const countMeh = score.statistics.meh ?? 0;
const countOk = score.statistics.ok ?? 0;

const totalHits = countMeh + countOk + countGreat + countMiss;
accuracy = totalHits > 0
? (countMeh * 50 + countOk * 100 + countGreat * 300) / (totalHits * 300)
: 1;

const ratioGreat = totalHits > 0 ? countGreat / totalHits : 1;
const ratioMeh = totalHits > 0 ? countMeh / totalHits : 1;

if (score.rank === 'F') {
rank = 'F';
} else if (ratioGreat === 1) {
rank = shouldHaveHiddenRank(score) ? 'XH' : 'X';
} else if (ratioGreat > 0.9 && ratioMeh <= 0.01 && countMiss === 0) {
rank = shouldHaveHiddenRank(score) ? 'SH' : 'S';
} else if ((ratioGreat > 0.8 && countMiss === 0) || ratioGreat > 0.9) {
rank = 'A';
} else if ((ratioGreat > 0.7 && countMiss === 0) || ratioGreat > 0.8) {
rank = 'B';
} else if (ratioGreat > 0.6) {
rank = 'C';
} else {
rank = 'D';
}
break;
}
// taiko
case 1: {
const countOk = score.statistics.ok ?? 0;

const totalHits = countOk + countGreat + countMiss;
accuracy = totalHits > 0
? (countOk * 150 + countGreat * 300) / (totalHits * 300)
: 1;

const ratioGreat = totalHits > 0 ? countGreat / totalHits : 1;

if (score.rank === 'F') {
rank = 'F';
} else if (ratioGreat === 1) {
rank = shouldHaveHiddenRank(score) ? 'XH' : 'X';
} else if (ratioGreat > 0.9 && countMiss === 0) {
rank = shouldHaveHiddenRank(score) ? 'SH' : 'S';
} else if ((ratioGreat > 0.8 && countMiss === 0) || ratioGreat > 0.9) {
rank = 'A';
} else if ((ratioGreat > 0.7 && countMiss === 0) || ratioGreat > 0.8) {
rank = 'B';
} else if (ratioGreat > 0.6) {
rank = 'C';
} else {
rank = 'D';
}
break;
}
// catch
case 2: {
const countLargeTickHit = score.statistics.large_tick_hit ?? 0;
const countSmallTickHit = score.statistics.small_tick_hit ?? 0;
const countSmallTickMiss = score.statistics.small_tick_miss ?? 0;

const totalHits = countSmallTickHit + countLargeTickHit + countGreat + countMiss + countSmallTickMiss;
accuracy = totalHits > 0
? (countSmallTickHit + countLargeTickHit + countGreat) / totalHits
: 1;

if (score.rank === 'F') {
rank = 'F';
} else if (accuracy === 1) {
rank = shouldHaveHiddenRank(score) ? 'XH' : 'X';
} else if (accuracy > 0.98) {
rank = shouldHaveHiddenRank(score) ? 'SH' : 'S';
} else if (accuracy > 0.94) {
rank = 'A';
} else if (accuracy > 0.9) {
rank = 'B';
} else if (accuracy > 0.85) {
rank = 'C';
} else {
rank = 'D';
}
break;
}
// mania
case 3: {
const countPerfect = score.statistics.perfect ?? 0;
const countGood = score.statistics.good ?? 0;
const countOk = score.statistics.ok ?? 0;
const countMeh = score.statistics.meh ?? 0;

const totalHits = countPerfect + countGood + countOk + countMeh + countGreat + countMiss;
accuracy = totalHits > 0
? ((countGreat + countPerfect) * 300 + countGood * 200 + countOk * 100 + countMeh * 50) / (totalHits * 300)
: 1;

if (score.rank === 'F') {
rank = 'F';
} else if (accuracy === 1) {
rank = shouldHaveHiddenRank(score) ? 'XH' : 'X';
} else if (accuracy > 0.95) {
rank = shouldHaveHiddenRank(score) ? 'SH' : 'S';
} else if (accuracy > 0.9) {
rank = 'A';
} else if (accuracy > 0.8) {
rank = 'B';
} else if (accuracy > 0.7) {
rank = 'C';
} else {
rank = 'D';
}
break;
}
default:
throw new Error('unknown score ruleset');
}

cached = cache[key] = { accuracy, rank };
}

return cached;
}
34 changes: 31 additions & 3 deletions resources/js/utils/score-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,31 @@ import { route } from 'laroute';
import core from 'osu-core-singleton';
import { rulesetName } from './beatmap-helper';
import { trans } from './lang';
import { legacyAccuracyAndRank } from './legacy-score-helper';

export function accuracy(score: SoloScoreJson) {
if (score.legacy_score_id == null || !core.userPreferences.get('legacy_score_only')) {
return score.accuracy;
}

return legacyAccuracyAndRank(score).accuracy;
}

export function canBeReported(score: SoloScoreJson) {
return (score.best_id != null || score.type === 'solo_score')
&& core.currentUser != null
&& score.user_id !== core.currentUser.id;
}

// Removes CL mod on legacy score if user has lazer mode disabled
export function filterMods(score: SoloScoreJson) {
if (score.legacy_score_id == null || !core.userPreferences.get('legacy_score_only')) {
return score.mods;
}

return score.mods.filter((mod) => mod.acronym !== 'CL');
}

// TODO: move to application state repository thingy later
export function hasMenu(score: SoloScoreJson) {
return canBeReported(score) || hasReplay(score) || hasShow(score) || core.scorePins.canBePinned(score);
Expand Down Expand Up @@ -92,6 +110,14 @@ export const modeAttributesMap: Record<GameMode, AttributeData[]> = {
],
};

export function rank(score: SoloScoreJson) {
if (score.legacy_score_id == null || !core.userPreferences.get('legacy_score_only')) {
return score.rank;
}

return legacyAccuracyAndRank(score).rank;
}

export function scoreDownloadUrl(score: SoloScoreJson) {
if (score.type === 'solo_score') {
return route('scores.download', { score: score.id });
Expand Down Expand Up @@ -123,7 +149,9 @@ export function scoreUrl(score: SoloScoreJson) {
}

export function totalScore(score: SoloScoreJson) {
return score.legacy_score_id == null
? score.total_score
: score.legacy_total_score;
if (score.legacy_score_id == null || !core.userPreferences.get('legacy_score_only')) {
return score.total_score;
}

return score.legacy_total_score;
}

0 comments on commit c696378

Please sign in to comment.