diff --git a/gitadora@asphyxia/README.md b/gitadora@asphyxia/README.md index 45499d7..8caecd1 100644 --- a/gitadora@asphyxia/README.md +++ b/gitadora@asphyxia/README.md @@ -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: { + "": { + update: [, ], // Highest new_skill value seen and its associated seq + diffs: { + "": { + perc: , // Highest achievement percentage + rank: , // Highest rank reached for the chart + clear: , // Whether the chart has been cleared + fc: , // Whether a full combo was achieved + ex: , // Whether an excellent was achieved + meter: "",// Best meter value as a stringified bigint + prog: , // 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 ============= diff --git a/gitadora@asphyxia/handlers/SharedScores.ts b/gitadora@asphyxia/handlers/SharedScores.ts new file mode 100644 index 0000000..f745e72 --- /dev/null +++ b/gitadora@asphyxia/handlers/SharedScores.ts @@ -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(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 { + const scoreDocs = await DB.Find(refid, { collection: 'scores', game }); + const mergedScores = scoreDocs.reduce((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(refid, { collection: 'scores', game, version: 'shared' }); + const mergedScores = mergeScoreCollections(existingShared?.scores ?? {}, scores); + + await persistSharedScores(refid, game, mergedScores); +} diff --git a/gitadora@asphyxia/handlers/profiles.ts b/gitadora@asphyxia/handlers/profiles.ts index 4ab4269..c61da9c 100644 --- a/gitadora@asphyxia/handlers/profiles.ts +++ b/gitadora@asphyxia/handlers/profiles.ts @@ -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(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) } diff --git a/gitadora@asphyxia/index.ts b/gitadora@asphyxia/index.ts index ec83d97..9d21d30 100644 --- a/gitadora@asphyxia/index.ts +++ b/gitadora@asphyxia/index.ts @@ -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", diff --git a/gitadora@asphyxia/utils/index.ts b/gitadora@asphyxia/utils/index.ts index 9642a69..6146db5 100644 --- a/gitadora@asphyxia/utils/index.ts +++ b/gitadora@asphyxia/utils/index.ts @@ -32,4 +32,8 @@ export function isAsphyxiaDebugMode() : boolean { export function isSharedFavoriteMusicEnabled() : boolean{ return Boolean(U.GetConfig("shared_favorite_songs")) -} \ No newline at end of file +} + +export function isSharedSongScoresEnabled() : boolean{ + return Boolean(U.GetConfig("shared_song_scores")) +}