Skip to content

Commit

Permalink
Merge branch 'master' into yeahbennou-touch-sr
Browse files Browse the repository at this point in the history
  • Loading branch information
Rian8337 committed Jun 4, 2024
2 parents 00800ec + 1d8195f commit 592852f
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 92 deletions.
136 changes: 87 additions & 49 deletions packages/osu-droid-replay-analyzer/src/ReplayAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,25 @@ import {
DroidAPIRequestBuilder,
DroidHitWindow,
MathUtils,
ModAuto,
ModAutopilot,
ModDifficultyAdjust,
ModDoubleTime,
ModEasy,
ModFlashlight,
ModHalfTime,
ModHardRock,
ModHidden,
ModNightCore,
ModNoFail,
ModPerfect,
ModPrecise,
ModReallyEasy,
ModRelax,
ModScoreV2,
ModSmallCircle,
ModSpeedUp,
ModSuddenDeath,
ModUtil,
Modes,
Slider,
Expand Down Expand Up @@ -43,6 +58,10 @@ export interface HitErrorInformation {
unstableRate: number;
}

interface Counter {
counter: number;
}

/**
* A replay analyzer that analyzes a replay from osu!droid.
*
Expand Down Expand Up @@ -137,13 +156,6 @@ export class ReplayAnalyzer {

private convertedBeatmap?: Beatmap;

// Sizes of primitive data types in Java (in bytes)
private readonly BYTE_LENGTH = 1;
private readonly SHORT_LENGTH = 2;
private readonly INT_LENGTH = 4;
private readonly FLOAT_LENGTH = 4;
private readonly LONG_LENGTH = 8;

constructor(values: {
/**
* The ID of the score.
Expand Down Expand Up @@ -400,35 +412,28 @@ export class ReplayAnalyzer {

// Merge all cursor movement and hit object data section into one for better control when parsing
const replayDataBuffer = Buffer.concat(replayDataBufferArray);
let bufferCounter = 0;
const bufferCounter: Counter = { counter: 0 };

const size = replayDataBuffer.readInt32BE(bufferCounter);
bufferCounter += this.INT_LENGTH;
const size = this.readInt(replayDataBuffer, bufferCounter);

// Parse movement data
for (let x = 0; x < size; x++) {
const moveSize = replayDataBuffer.readInt32BE(bufferCounter);
bufferCounter += this.INT_LENGTH;
const moveSize = this.readInt(replayDataBuffer, bufferCounter);
const time: number[] = [];
const x: number[] = [];
const y: number[] = [];
const id: MovementType[] = [];
for (let i = 0; i < moveSize; i++) {
time[i] = replayDataBuffer.readInt32BE(bufferCounter);
bufferCounter += this.INT_LENGTH;
time[i] = this.readInt(replayDataBuffer, bufferCounter);
id[i] = time[i] & 3;
time[i] >>= 2;
if (id[i] !== MovementType.up) {
if (resultObject.replayVersion >= 5) {
x[i] = replayDataBuffer.readFloatBE(bufferCounter);
bufferCounter += this.FLOAT_LENGTH;
y[i] = replayDataBuffer.readFloatBE(bufferCounter);
bufferCounter += this.FLOAT_LENGTH;
x[i] = this.readFloat(replayDataBuffer, bufferCounter);
y[i] = this.readFloat(replayDataBuffer, bufferCounter);
} else {
x[i] = replayDataBuffer.readInt16BE(bufferCounter);
bufferCounter += this.SHORT_LENGTH;
y[i] = replayDataBuffer.readInt16BE(bufferCounter);
bufferCounter += this.SHORT_LENGTH;
x[i] = this.readShort(replayDataBuffer, bufferCounter);
y[i] = this.readShort(replayDataBuffer, bufferCounter);
}
} else {
x[i] = -1;
Expand All @@ -446,8 +451,10 @@ export class ReplayAnalyzer {
);
}

const replayObjectLength = replayDataBuffer.readInt32BE(bufferCounter);
bufferCounter += this.INT_LENGTH;
const replayObjectLength = this.readInt(
replayDataBuffer,
bufferCounter,
);

// Parse result data
for (let i = 0; i < replayObjectLength; i++) {
Expand All @@ -457,18 +464,17 @@ export class ReplayAnalyzer {
result: HitResult.miss,
};

replayObjectData.accuracy =
replayDataBuffer.readInt16BE(bufferCounter);
bufferCounter += this.SHORT_LENGTH;
const len = replayDataBuffer.readInt8(bufferCounter);
bufferCounter += this.BYTE_LENGTH;
replayObjectData.accuracy = this.readShort(
replayDataBuffer,
bufferCounter,
);
const len = this.readByte(replayDataBuffer, bufferCounter);

if (len > 0) {
const bytes: number[] = [];

for (let j = 0; j < len; j++) {
bytes.push(replayDataBuffer.readInt8(bufferCounter));
bufferCounter += this.BYTE_LENGTH;
bytes.push(this.readByte(replayDataBuffer, bufferCounter));
}
// Int/int division in Java; numbers must be truncated to get actual number
for (let j = 0; j < len * 8; j++) {
Expand All @@ -481,9 +487,10 @@ export class ReplayAnalyzer {
}

if (resultObject.replayVersion >= 1) {
replayObjectData.result =
replayDataBuffer.readInt8(bufferCounter);
bufferCounter += this.BYTE_LENGTH;
replayObjectData.result = this.readByte(
replayDataBuffer,
bufferCounter,
);
}

resultObject.hitObjectData.push(replayObjectData);
Expand Down Expand Up @@ -623,21 +630,24 @@ export class ReplayAnalyzer {
*/
private convertDroidMods(replayMods: string[]): string {
const replayModsConstants = {
MOD_NOFAIL: "n",
MOD_EASY: "e",
MOD_HIDDEN: "h",
MOD_HARDROCK: "r",
MOD_DOUBLETIME: "d",
MOD_HALFTIME: "t",
MOD_NIGHTCORE: "c",
MOD_PRECISE: "s",
MOD_SMALLCIRCLE: "m",
MOD_SPEEDUP: "b",
MOD_REALLYEASY: "l",
MOD_PERFECT: "f",
MOD_SUDDENDEATH: "u",
MOD_SCOREV2: "v",
MOD_FLASHLIGHT: "i",
MOD_AUTO: new ModAuto().droidString,
MOD_AUTOPILOT: new ModAutopilot().droidString,
MOD_NOFAIL: new ModNoFail().droidString,
MOD_EASY: new ModEasy().droidString,
MOD_HIDDEN: new ModHidden().droidString,
MOD_HARDROCK: new ModHardRock().droidString,
MOD_DOUBLETIME: new ModDoubleTime().droidString,
MOD_HALFTIME: new ModHalfTime().droidString,
MOD_NIGHTCORE: new ModNightCore().droidString,
MOD_PRECISE: new ModPrecise().droidString,
MOD_SMALLCIRCLE: new ModSmallCircle().droidString,
MOD_SPEEDUP: new ModSpeedUp().droidString,
MOD_REALLYEASY: new ModReallyEasy().droidString,
MOD_RELAX: new ModRelax().droidString,
MOD_PERFECT: new ModPerfect().droidString,
MOD_SUDDENDEATH: new ModSuddenDeath().droidString,
MOD_SCOREV2: new ModScoreV2().droidString,
MOD_FLASHLIGHT: new ModFlashlight().droidString,
};

let modString = "";
Expand Down Expand Up @@ -794,4 +804,32 @@ export class ReplayAnalyzer {
this.sliderCheesePenalty = sliderCheeseChecker.check();
this.hasBeenCheckedForSliderCheesing = true;
}

private readByte(buffer: Buffer, counter: Counter): number {
const num = buffer.readInt8(counter.counter);
counter.counter += 1;

return num;
}

private readShort(buffer: Buffer, counter: Counter): number {
const num = buffer.readInt16BE(counter.counter);
counter.counter += 2;

return num;
}

private readInt(buffer: Buffer, counter: Counter): number {
const num = buffer.readInt32BE(counter.counter);
counter.counter += 4;

return num;
}

private readFloat(buffer: Buffer, counter: Counter): number {
const num = buffer.readFloatBE(counter.counter);
counter.counter += 4;

return num;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,11 @@ export class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiffi

// Scale the aim value with deviation.
aimValue *=
1.05 *
Math.sqrt(ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)));
1.025 *
Math.pow(
ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)),
0.475,
);

// OD 7 SS stays the same.
aimValue *= 0.98 + Math.pow(7, 2) / 2500;
Expand Down Expand Up @@ -320,10 +323,10 @@ export class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiffi

// Scale the tap value with tap deviation.
tapValue *=
1.1 *
1.05 *
Math.pow(
ErrorFunction.erf(20 / (Math.SQRT2 * adjustedDeviation)),
0.625,
0.6,
);

// Additional scaling for tap value based on average BPM and how "vibroable" the beatmap is.
Expand Down Expand Up @@ -359,7 +362,7 @@ export class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiffi
return 0;
}

let accuracyValue = 800 * Math.exp(-0.1 * this._deviation);
let accuracyValue = 650 * Math.exp(-0.1 * this._deviation);

const ncircles = this.difficultyAttributes.mods.some(
(m) => m instanceof ModScoreV2,
Expand Down Expand Up @@ -465,10 +468,10 @@ export class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiffi

// Scale the visual value with deviation.
visualValue *=
1.065 *
1.05 *
Math.pow(
ErrorFunction.erf(25 / (Math.SQRT2 * this._deviation)),
0.8,
0.775,
);

// OD 5 SS stays the same.
Expand All @@ -492,8 +495,9 @@ export class DroidPerformanceCalculator extends PerformanceCalculator<DroidDiffi
}

return (
0.94 /
(this.effectiveMissCount / (2 * Math.sqrt(difficultStrainCount)) +
0.96 /
(this.effectiveMissCount /
(4 * Math.pow(Math.log(difficultStrainCount), 0.94)) +
1)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ export abstract class DroidVisualEvaluator {
const pixelTravelDistance =
current.object.lazyTravelDistance / scalingFactor;
const currentVelocity = pixelTravelDistance / current.travelTime;
const spanTravelDistance =
pixelTravelDistance / current.object.spanCount;

strain +=
// Reward sliders based on velocity, while also avoiding overbuffing extremely fast sliders.
Math.min(6, currentVelocity * 1.5) *
// Longer sliders require more reading.
(pixelTravelDistance / 100);
(spanTravelDistance / 100);

let cumulativeStrainTime = 0;

Expand All @@ -109,9 +111,11 @@ export abstract class DroidVisualEvaluator {
}

// Invert the scaling factor to determine the true travel distance independent of circle size.
const pixelTravelDistance =
const lastPixelTravelDistance =
last.object.lazyTravelDistance / scalingFactor;
const lastVelocity = pixelTravelDistance / last.travelTime;
const lastVelocity = lastPixelTravelDistance / last.travelTime;
const lastSpanTravelDistance =
lastPixelTravelDistance / last.object.spanCount;

strain +=
// Reward past sliders based on velocity changes, while also
Expand All @@ -121,7 +125,7 @@ export abstract class DroidVisualEvaluator {
2.5 * Math.abs(currentVelocity - lastVelocity),
) *
// Longer sliders require more reading.
(pixelTravelDistance / 125) *
(lastSpanTravelDistance / 125) *
// Avoid overbuffing past sliders.
Math.min(1, 300 / cumulativeStrainTime);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export abstract class DroidSkill extends StrainSkill {
return this._objectStrains;
}

private difficulty = 0;

constructor(mods: Mod[], objectCount: number) {
super(mods);

Expand All @@ -33,18 +35,18 @@ export abstract class DroidSkill extends StrainSkill {
* The result is scaled by clock rate as it affects the total number of strains.
*/
countDifficultStrains(): number {
if (this._objectStrains.length === 0) {
if (this.difficulty === 0) {
return 0;
}

const maxStrain = Math.max(...this._objectStrains);

if (maxStrain === 0) {
return 0;
}
// This is what the top strain is if all strain values were identical.
const consistentTopStrain = this.difficulty / 10;

// Use a weighted sum of all strains.
return this._objectStrains.reduce(
(total, next) => total + Math.pow(next / maxStrain, 4),
(total, next) =>
total +
1.1 / (1 + Math.exp(-10 * (next / consistentTopStrain - 0.88))),
0,
);
}
Expand Down Expand Up @@ -85,16 +87,16 @@ export abstract class DroidSkill extends StrainSkill {

// Math here preserves the property that two notes of equal difficulty x, we have their summed difficulty = x * starsPerDouble.
// This also applies to two sets of notes with equal difficulty.
return Math.pow(
strains.reduce((a, v) => {
if (v <= 0) {
return a;
}

return a + Math.pow(v, 1 / Math.log2(this.starsPerDouble));
}, 0),
Math.log2(this.starsPerDouble),
);
this.difficulty = 0;

for (const strain of strains) {
this.difficulty += Math.pow(
strain,
1 / Math.log2(this.starsPerDouble),
);
}

return Math.pow(this.difficulty, Math.log2(this.starsPerDouble));
}

/**
Expand Down
Loading

0 comments on commit 592852f

Please sign in to comment.