mirror of
https://github.com/asphyxia-core/plugins.git
synced 2026-03-21 17:34:46 -05:00
Add shared song score aggregation support
This commit is contained in:
parent
4aeb1249f6
commit
1bbebfb033
|
|
@ -29,6 +29,34 @@ Known Issues
|
|||
* Special Premium Encore on Nextage is unimplemented. However, a workaround is available. Try it.
|
||||
* Friends and Rivals are unimplemented.
|
||||
|
||||
Shared Data Options
|
||||
===================
|
||||
Two experimental options allow operators to share data across versions:
|
||||
|
||||
* **Shared Favorite Songs** (`shared_favorite_songs`, default: `false`): When enabled, favorite lists are unified across Guitar Freaks, DrumMania, and supported versions.
|
||||
* **Shared Song Scores** (`shared_song_scores`, default: `false`): When enabled, the server merges the best results for each chart across every stored version and saves them under a shared version identifier. The merged record uses the following shape (fields marked with `//` describe their meaning):
|
||||
|
||||
```
|
||||
scores: {
|
||||
"<musicid>": {
|
||||
update: [<seq>, <new_skill>], // Highest new_skill value seen and its associated seq
|
||||
diffs: {
|
||||
"<seq>": {
|
||||
perc: <number>, // Highest achievement percentage
|
||||
rank: <number>, // Highest rank reached for the chart
|
||||
clear: <boolean>, // Whether the chart has been cleared
|
||||
fc: <boolean>, // Whether a full combo was achieved
|
||||
ex: <boolean>, // Whether an excellent was achieved
|
||||
meter: "<string>",// Best meter value as a stringified bigint
|
||||
prog: <number>, // Highest progression value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Scores are stored under `version: "shared"` but are automatically applied to the active module when loading a profile, ensuring players benefit from their best combined results regardless of the client version.
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
|
|
|
|||
90
gitadora@asphyxia/handlers/SharedScores.ts
Normal file
90
gitadora@asphyxia/handlers/SharedScores.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { PLUGIN_VER } from "../const";
|
||||
import { Scores } from "../models/scores";
|
||||
|
||||
type ScoreDiff = Scores['scores'][string]['diffs'][string];
|
||||
type ScoreEntry = Scores['scores'][string];
|
||||
|
||||
function selectBetterMeter(existing?: string, incoming?: string): string {
|
||||
if (!incoming) return existing ?? "0";
|
||||
if (!existing) return incoming;
|
||||
|
||||
try {
|
||||
return BigInt(incoming) > BigInt(existing) ? incoming : existing;
|
||||
} catch (e) {
|
||||
return incoming || existing;
|
||||
}
|
||||
}
|
||||
|
||||
function mergeScoreDiff(existing: ScoreDiff | undefined, incoming: ScoreDiff): ScoreDiff {
|
||||
if (!existing) return incoming;
|
||||
|
||||
return {
|
||||
perc: Math.max(existing.perc ?? 0, incoming.perc ?? 0),
|
||||
rank: Math.max(existing.rank ?? 0, incoming.rank ?? 0),
|
||||
meter: selectBetterMeter(existing.meter, incoming.meter),
|
||||
prog: Math.max(existing.prog ?? 0, incoming.prog ?? 0),
|
||||
clear: (existing.clear ?? false) || (incoming.clear ?? false),
|
||||
fc: (existing.fc ?? false) || (incoming.fc ?? false),
|
||||
ex: (existing.ex ?? false) || (incoming.ex ?? false),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeScoreEntry(existing: ScoreEntry | undefined, incoming: ScoreEntry): ScoreEntry {
|
||||
const mergedDiffs: ScoreEntry['diffs'] = existing ? { ...existing.diffs } : {};
|
||||
|
||||
for (const [seq, diff] of Object.entries(incoming.diffs)) {
|
||||
mergedDiffs[seq] = mergeScoreDiff(mergedDiffs[seq], diff);
|
||||
}
|
||||
|
||||
const mergedUpdate = existing?.update ? [...existing.update] : [0, 0];
|
||||
if (incoming.update && (mergedUpdate[1] ?? 0) < incoming.update[1]) {
|
||||
mergedUpdate[0] = incoming.update[0];
|
||||
mergedUpdate[1] = incoming.update[1];
|
||||
}
|
||||
|
||||
return {
|
||||
update: mergedUpdate,
|
||||
diffs: mergedDiffs,
|
||||
};
|
||||
}
|
||||
|
||||
function mergeScoreCollections(target: Scores['scores'], incoming: Scores['scores']): Scores['scores'] {
|
||||
const merged = { ...target } as Scores['scores'];
|
||||
|
||||
for (const [mid, entry] of Object.entries(incoming)) {
|
||||
merged[mid] = mergeScoreEntry(merged[mid], entry);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
async function persistSharedScores(refid: string, game: 'gf' | 'dm', scores: Scores['scores']) {
|
||||
await DB.Upsert<Scores>(refid, { collection: 'scores', game, version: 'shared' }, {
|
||||
collection: 'scores',
|
||||
version: 'shared',
|
||||
pluginVer: PLUGIN_VER,
|
||||
game,
|
||||
scores,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and merge scores across all versions for a player/game pair and persist them under version "shared".
|
||||
*/
|
||||
export async function getMergedSharedScores(refid: string, game: 'gf' | 'dm'): Promise<Scores['scores']> {
|
||||
const scoreDocs = await DB.Find<Scores>(refid, { collection: 'scores', game });
|
||||
const mergedScores = scoreDocs.reduce<Scores['scores']>((acc, doc) => mergeScoreCollections(acc, doc.scores), {} as Scores['scores']);
|
||||
|
||||
await persistSharedScores(refid, game, mergedScores);
|
||||
return mergedScores;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the provided score set into the shared scores document for the player/game pair.
|
||||
*/
|
||||
export async function mergeScoresIntoShared(refid: string, game: 'gf' | 'dm', scores: Scores['scores']) {
|
||||
const existingShared = await DB.FindOne<Scores>(refid, { collection: 'scores', game, version: 'shared' });
|
||||
const mergedScores = mergeScoreCollections(existingShared?.scores ?? {}, scores);
|
||||
|
||||
await persistSharedScores(refid, game, mergedScores);
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ import { getDefaultScores, Scores } from "../models/scores";
|
|||
|
||||
import { PLUGIN_VER } from "../const";
|
||||
import Logger from "../utils/logger"
|
||||
import { isAsphyxiaDebugMode } from "../utils/index";
|
||||
import { isAsphyxiaDebugMode, isSharedSongScoresEnabled } from "../utils/index";
|
||||
import { SecretMusicEntry } from "../models/secretmusicentry";
|
||||
import { CheckPlayerResponse, getCheckPlayerResponse } from "../models/Responses/checkplayerresponse";
|
||||
import { getPlayerStickerResponse, PlayerStickerResponse } from "../models/Responses/playerstickerresponse";
|
||||
|
|
@ -20,6 +20,7 @@ import { getDefaultBattleDataResponse } from "../models/Responses/battledataresp
|
|||
import { applySharedFavoriteMusicToExtra, saveSharedFavoriteMusicFromExtra } from "./FavoriteMusic";
|
||||
import { getPlayerRecordResponse } from "../models/Responses/playerrecordresponse";
|
||||
import { getPlayerPlayInfoResponse, PlayerPlayInfoResponse } from "../models/Responses/playerplayinforesponse";
|
||||
import { getMergedSharedScores, mergeScoresIntoShared } from "./SharedScores";
|
||||
|
||||
const logger = new Logger("profiles")
|
||||
|
||||
|
|
@ -72,6 +73,7 @@ export const getPlayer: EPR = async (info, data, send) => {
|
|||
const time = BigInt(31536000);
|
||||
const dm = isDM(info);
|
||||
const game = dm ? 'dm' : 'gf';
|
||||
const sharedScoresEnabled = isSharedSongScoresEnabled();
|
||||
|
||||
logger.debugInfo(`Loading ${game} profile for player ${no} with refid: ${refid}`)
|
||||
const name = await DB.FindOne<PlayerInfo>(refid, {
|
||||
|
|
@ -84,8 +86,8 @@ export const getPlayer: EPR = async (info, data, send) => {
|
|||
const gfRecord = await getRecord(refid, version, 'gf')
|
||||
const dmExtra = await getExtra(refid, version, 'dm')
|
||||
const gfExtra = await getExtra(refid, version, 'gf')
|
||||
const dmScores = (await getScore(refid, version, 'dm')).scores
|
||||
const gfScores = (await getScore(refid, version, 'gf')).scores
|
||||
const dmScores = sharedScoresEnabled ? await getMergedSharedScores(refid, 'dm') : (await getScore(refid, version, 'dm')).scores
|
||||
const gfScores = sharedScoresEnabled ? await getMergedSharedScores(refid, 'gf') : (await getScore(refid, version, 'gf')).scores
|
||||
|
||||
const profile = dm ? dmProfile : gfProfile;
|
||||
const extra = dm ? dmExtra : gfExtra;
|
||||
|
|
@ -524,6 +526,7 @@ export const savePlayers: EPR = async (info, data, send) => {
|
|||
const version = getVersion(info);
|
||||
const dm = isDM(info);
|
||||
const game = dm ? 'dm' : 'gf';
|
||||
const sharedScoresEnabled = isSharedSongScoresEnabled();
|
||||
|
||||
let players = $(data).elements("player")
|
||||
|
||||
|
|
@ -549,7 +552,7 @@ export const savePlayers: EPR = async (info, data, send) => {
|
|||
throw "Request data is missing required parameter: player.refid"
|
||||
}
|
||||
|
||||
await saveSinglePlayer(player, refid, no, version, game);
|
||||
await saveSinglePlayer(player, refid, no, version, game, sharedScoresEnabled);
|
||||
|
||||
let ranking = await getPlayerRanking(refid, version, game)
|
||||
let responsePart = getSaveProfileResponse(no, ranking)
|
||||
|
|
@ -569,7 +572,7 @@ export const savePlayers: EPR = async (info, data, send) => {
|
|||
}
|
||||
};
|
||||
|
||||
async function saveSinglePlayer(dataplayer: KDataReader, refid: string, no: number, version: string, game: 'gf' | 'dm')
|
||||
async function saveSinglePlayer(dataplayer: KDataReader, refid: string, no: number, version: string, game: 'gf' | 'dm', sharedScoresEnabled: boolean)
|
||||
{
|
||||
logger.debugInfo(`Saving ${game} profile for player ${no} with refid: ${refid}`)
|
||||
const profile = await getProfile(refid, version, game) as any;
|
||||
|
|
@ -712,7 +715,11 @@ async function saveSinglePlayer(dataplayer: KDataReader, refid: string, no: numb
|
|||
logStagesPlayed(playedStages)
|
||||
|
||||
const scores = await updatePlayerScoreCollection(refid, playedStages, version, game)
|
||||
await saveScore(refid, version, game, scores);
|
||||
await saveScore(refid, version, game, scores);
|
||||
|
||||
if (sharedScoresEnabled) {
|
||||
await mergeScoresIntoShared(refid, game, scores);
|
||||
}
|
||||
await saveSharedFavoriteMusicFromExtra(refid, extra)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,12 +40,19 @@ export function register() {
|
|||
|
||||
R.Config("shared_favorite_songs", {
|
||||
name: "Shared Favorite Songs (Experimental)",
|
||||
desc: "If disabled, players will be able to keep separate lists of favorite songs for each version of Gitadora, as well as between Guitar Freaks and Drummania. " +
|
||||
desc: "If disabled, players will be able to keep separate lists of favorite songs for each version of Gitadora, as well as between Guitar Freaks and Drummania. " +
|
||||
"Enable this option to have a single unified list of favorite songs for both games, and across all versions. Default is false, to match original arcade behaviour.",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
})
|
||||
|
||||
R.Config("shared_song_scores", {
|
||||
name: "Shared Song Scores (Experimental)",
|
||||
desc: "If disabled, players will keep separate scoreboards per version. Enable to merge best scores across all versions and games into a shared store.",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
})
|
||||
|
||||
R.DataFile("data/mdb/custom.xml", {
|
||||
accept: ".xml",
|
||||
name: "Custom MDB",
|
||||
|
|
|
|||
|
|
@ -32,4 +32,8 @@ export function isAsphyxiaDebugMode() : boolean {
|
|||
|
||||
export function isSharedFavoriteMusicEnabled() : boolean{
|
||||
return Boolean(U.GetConfig("shared_favorite_songs"))
|
||||
}
|
||||
}
|
||||
|
||||
export function isSharedSongScoresEnabled() : boolean{
|
||||
return Boolean(U.GetConfig("shared_song_scores"))
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user