diff --git a/OpenTaiko/Databases/UnlockablesDocumentation.txt b/OpenTaiko/Databases/UnlockablesDocumentation.txt index ed605d75..09c290ac 100644 --- a/OpenTaiko/Databases/UnlockablesDocumentation.txt +++ b/OpenTaiko/Databases/UnlockablesDocumentation.txt @@ -15,4 +15,16 @@ * e : "Equal" * me : "More or equal" (Default) * m : "More than" -* d : "Different" \ No newline at end of file +* d : "Different" + +== PlayMods flags == + +A 8 bytes value, from upper to lower bytes: +* Byte 1: Scroll speed: The mod value (x0.1 = 0, +1 per 0.1 increment) capped to 255 +* Byte 2: Doron: The mod value (0 - 2 for None/Doron/Stealth) +* Byte 3: Random: The mod value (0 - 5 for None/Mirror/Random/SuperRandom/HyperRandom) +* Byte 4: Song speed: The mod value (x1 is 20, +-1 per 0.05 change) capped to 255 +* Byte 5: Timing : The mod value (0 - 5 from Loose to Rigorous) +* Byte 6: Just: The mod value (0 - 2 for None/Just/Safe) +* Byte 7: Unused +* Byte 8: Fun mods: The mod value (0 - 2 for None/Avalanche/Minesweeper) \ No newline at end of file diff --git a/OpenTaiko/Saves.db3 b/OpenTaiko/Saves.db3 index a7a531ac..a31f0b19 100644 Binary files a/OpenTaiko/Saves.db3 and b/OpenTaiko/Saves.db3 differ diff --git a/OpenTaiko/src/Helpers/HDatabaseHelpers.cs b/OpenTaiko/src/Helpers/HDatabaseHelpers.cs index 1fce9564..f254def1 100644 --- a/OpenTaiko/src/Helpers/HDatabaseHelpers.cs +++ b/OpenTaiko/src/Helpers/HDatabaseHelpers.cs @@ -1,25 +1,66 @@ using System; using System.Collections.Generic; +using System.Data; using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Data.Sqlite; +using Newtonsoft.Json; namespace TJAPlayer3 { public static class HDatabaseHelpers { + private static string _savesDBPath = @$"{TJAPlayer3.strEXEのあるフォルダ}Saves.db3"; + private static SqliteConnection SavesDBConnection = new SqliteConnection(@$"Data Source={_savesDBPath}"); + + public class CBestPlayRecord + { + public string ChartUniqueId = "none"; + public string ChartGenre = "none"; + public string Charter = "none"; + public string Artist = "none"; + public Int64 PlayMods = 0; + public Int64 ChartDifficulty = 3; + public Int64 ChartLevel = 8; + public Int64 ClearStatus = -1; + public Int64 ScoreRank = -1; + public Int64 HighScore = 0; + public Int64 TowerBestFloor = 0; + public List DanExam1 = new List { -1 }; + public List DanExam2 = new List { -1 }; + public List DanExam3 = new List { -1 }; + public List DanExam4 = new List { -1 }; + public List DanExam5 = new List { -1 }; + public List DanExam6 = new List { -1 }; + public List DanExam7 = new List { -1 }; + public Int64 PlayCount = 1; + public Int64 HighScoreGoodCount = 0; + public Int64 HighScoreOkCount = 0; + public Int64 HighScoreBadCount = 0; + public Int64 HighScoreMaxCombo = 0; + public Int64 HighScoreRollCount = 0; + public Int64 HighScoreADLibCount = 0; + public Int64 HighScoreBoomCount = 0; + } + + public static SqliteConnection GetSavesDBConnection() + { + if (SavesDBConnection != null && SavesDBConnection.State == ConnectionState.Closed) SavesDBConnection.Open(); + return SavesDBConnection; + } + public static List GetAvailableLanguage(SqliteConnection connection, string prefix) { List _translations = new List(); foreach (string cd in CLangManager.Langcodes) { - var chk = connection.CreateCommand(); + SqliteCommand chk = connection.CreateCommand(); chk.CommandText = @$" SELECT count(*) FROM sqlite_master WHERE type='table' AND name='{prefix}_{cd}'; "; - var chkreader = chk.ExecuteReader(); + SqliteDataReader chkreader = chk.ExecuteReader(); while (chkreader.Read()) { if (chkreader.GetInt32(0) > 0) @@ -28,5 +69,196 @@ namespace TJAPlayer3 } return _translations; } + + public static void RegisterPlay(int player, int clearStatus, int scoreRank) + { + SqliteConnection connection = GetSavesDBConnection(); + SaveFile.Data saveData = TJAPlayer3.SaveFileInstances[TJAPlayer3.GetActualPlayer(player)].data; + CBestPlayRecord currentPlay = new CBestPlayRecord(); + var choosenSong = TJAPlayer3.stageSongSelect.rChoosenSong; + var choosenDifficulty = TJAPlayer3.stageSongSelect.nChoosenSongDifficulty[player]; + var chartScore = TJAPlayer3.stage演奏ドラム画面.CChartScore[player]; + List[] danResults = new List[7] { new List(), new List(), new List(), new List(), new List(), new List(), new List() }; + + // 1st step: Init best play record class + + { + currentPlay.ChartUniqueId = choosenSong.uniqueId.data.id; + currentPlay.ChartGenre = choosenSong.strジャンル; + currentPlay.Charter = choosenSong.strNotesDesigner[choosenDifficulty]; + currentPlay.Artist = choosenSong.strサブタイトル; // There is no direct Artist tag on the .tja format, so we directly use the subtitle as a guess + currentPlay.PlayMods = ModIcons.tModsToPlayModsFlags(player); + currentPlay.ChartDifficulty = choosenDifficulty; + currentPlay.ChartLevel = choosenSong.arスコア[choosenDifficulty].譜面情報.nレベル[choosenDifficulty]; + currentPlay.ClearStatus = clearStatus; + currentPlay.ScoreRank = scoreRank; + currentPlay.HighScore = chartScore.nScore; + if (choosenDifficulty == (int)Difficulty.Tower) currentPlay.TowerBestFloor = CFloorManagement.LastRegisteredFloor; + if (choosenDifficulty == (int)Difficulty.Dan) + { + for (int i = 0; i < TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs.Count; i++) + { + for (int j = 0; j < TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C.Length; j++) + { + if (TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C[j] != null) + { + int amount = TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C[j].GetAmount(); + danResults[j].Add(amount); + } + } + } + } + currentPlay.PlayCount = 1; // Will be directly added to the current instance if exists + currentPlay.HighScoreGoodCount = chartScore.nGreat; + currentPlay.HighScoreOkCount = chartScore.nGood; + currentPlay.HighScoreBadCount = chartScore.nMiss; + currentPlay.HighScoreMaxCombo = TJAPlayer3.stage演奏ドラム画面.actCombo.n現在のコンボ数.最高値[player]; + currentPlay.HighScoreRollCount = chartScore.nRoll; + currentPlay.HighScoreADLibCount = chartScore.nADLIB; + currentPlay.HighScoreBoomCount = chartScore.nMine; + } + + // 2nd step: Overwrite the instance with best play results if exists + { + SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = + @$" + SELECT * FROM best_plays WHERE ChartUniqueId='{currentPlay.ChartUniqueId}' AND PlayMods={currentPlay.PlayMods} and ChartDifficulty={currentPlay.ChartDifficulty}; + "; + SqliteDataReader reader = cmd.ExecuteReader(); + while (reader.Read()) + { + // Overwrite multiple variables at once if the highscore is replaced + Int64 _highscore = (Int64)reader["HighScore"]; + if (_highscore > currentPlay.HighScore) + { + currentPlay.HighScore = _highscore; + currentPlay.HighScoreGoodCount = (Int64)reader["HighScoreGoodCount"]; + currentPlay.HighScoreOkCount = (Int64)reader["HighScoreOkCount"]; + currentPlay.HighScoreBadCount = (Int64)reader["HighScoreBadCount"]; + currentPlay.HighScoreMaxCombo = (Int64)reader["HighScoreMaxCombo"]; + currentPlay.HighScoreRollCount = (Int64)reader["HighScoreRollCount"]; + currentPlay.HighScoreADLibCount = (Int64)reader["HighScoreADLibCount"]; + currentPlay.HighScoreBoomCount = (Int64)reader["HighScoreBoomCount"]; + } + currentPlay.ClearStatus = Math.Max(currentPlay.ClearStatus, (Int64)reader["ClearStatus"]); + currentPlay.ScoreRank = Math.Max(currentPlay.ScoreRank, (Int64)reader["ScoreRank"]); + if (choosenDifficulty == (int)Difficulty.Tower) currentPlay.TowerBestFloor = Math.Max(currentPlay.TowerBestFloor, (Int64)reader["TowerBestFloor"]); + if (choosenDifficulty == (int)Difficulty.Dan) + { + List[] oldDanResults = new List[7] + { + JsonConvert.DeserializeObject>((string)reader["DanExam1"]) ?? new List { -1 }, + JsonConvert.DeserializeObject>((string)reader["DanExam2"]) ?? new List { -1 }, + JsonConvert.DeserializeObject>((string)reader["DanExam3"]) ?? new List { -1 }, + JsonConvert.DeserializeObject>((string)reader["DanExam4"]) ?? new List { -1 }, + JsonConvert.DeserializeObject>((string)reader["DanExam5"]) ?? new List { -1 }, + JsonConvert.DeserializeObject>((string)reader["DanExam6"]) ?? new List { -1 }, + JsonConvert.DeserializeObject>((string)reader["DanExam7"]) ?? new List { -1 } + }; + for (int i = 0; i < TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs.Count; i++) + { + for (int j = 0; j < TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C.Length; j++) + { + if (TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C[j] != null) + { + int amount = danResults[j][i]; + + if (i < oldDanResults[j].Count) + { + int current = oldDanResults[j][i]; + if (current == -1) + { + danResults[j][i] = amount; + } + else if (TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C[j].GetExamRange() == Exam.Range.More) + { + danResults[j][i] = Math.Max(amount, current); + } + else if (TJAPlayer3.stageSongSelect.rChoosenSong.DanSongs[i].Dan_C[j].GetExamRange() == Exam.Range.Less) + { + danResults[j][i] = Math.Min(amount, current); + } + } + + } + } + } + } + } + } + + // Intermede: Dan results to Dan exams + { + if (choosenDifficulty == (int)Difficulty.Dan) + { + currentPlay.DanExam1 = danResults[0]; + currentPlay.DanExam2 = danResults[1]; + currentPlay.DanExam3 = danResults[2]; + currentPlay.DanExam4 = danResults[3]; + currentPlay.DanExam5 = danResults[4]; + currentPlay.DanExam6 = danResults[5]; + currentPlay.DanExam7 = danResults[6]; + } + } + + // 3rd step: Insert/Update to database + { + SqliteCommand cmd = connection.CreateCommand(); + cmd.CommandText = $@" + INSERT INTO best_plays(ChartUniqueId,ChartGenre,Charter,Artist,PlayMods,ChartDifficulty,ChartLevel,ClearStatus,ScoreRank,HighScore,SaveId,TowerBestFloor,DanExam1,DanExam2,DanExam3,DanExam4,DanExam5,DanExam6,DanExam7,PlayCount,HighScoreGoodCount,HighScoreOkCount,HighScoreBadCount,HighScoreMaxCombo,HighScoreRollCount,HighScoreADLibCount,HighScoreBoomCount) + VALUES( + '{currentPlay.ChartUniqueId}', + '{currentPlay.ChartGenre}', + '{currentPlay.Charter}', + '{currentPlay.Artist}', + {currentPlay.PlayMods}, + {currentPlay.ChartDifficulty}, + {currentPlay.ChartLevel}, + {currentPlay.ClearStatus}, + {currentPlay.ScoreRank}, + {currentPlay.HighScore}, + {saveData.SaveId}, + {currentPlay.TowerBestFloor}, + '{@$"[{String.Join(",", currentPlay.DanExam1.ToArray())}]"}', + '{@$"[{String.Join(",", currentPlay.DanExam2.ToArray())}]"}', + '{@$"[{String.Join(",", currentPlay.DanExam3.ToArray())}]"}', + '{@$"[{String.Join(",", currentPlay.DanExam4.ToArray())}]"}', + '{@$"[{String.Join(",", currentPlay.DanExam5.ToArray())}]"}', + '{@$"[{String.Join(",", currentPlay.DanExam6.ToArray())}]"}', + '{@$"[{String.Join(",", currentPlay.DanExam7.ToArray())}]"}', + {currentPlay.PlayCount}, + {currentPlay.HighScoreGoodCount}, + {currentPlay.HighScoreOkCount}, + {currentPlay.HighScoreBadCount}, + {currentPlay.HighScoreMaxCombo}, + {currentPlay.HighScoreRollCount}, + {currentPlay.HighScoreADLibCount}, + {currentPlay.HighScoreBoomCount} + ) + ON CONFLICT(ChartUniqueId,ChartDifficulty,PlayMods) DO UPDATE SET + PlayCount=best_plays.PlayCount+1, + ClearStatus=EXCLUDED.ClearStatus, + ScoreRank=EXCLUDED.ScoreRank, + HighScore=EXCLUDED.HighScore, + HighScoreGoodCount=EXCLUDED.HighScoreGoodCount, + HighScoreOkCount=EXCLUDED.HighScoreOkCount, + HighScoreBadCount=EXCLUDED.HighScoreBadCount, + HighScoreMaxCombo=EXCLUDED.HighScoreMaxCombo, + HighScoreRollCount=EXCLUDED.HighScoreRollCount, + HighScoreADLibCount=EXCLUDED.HighScoreADLibCount, + HighScoreBoomCount=EXCLUDED.HighScoreBoomCount, + TowerBestFloor=EXCLUDED.TowerBestFloor, + DanExam1=EXCLUDED.DanExam1, + DanExam2=EXCLUDED.DanExam2, + DanExam3=EXCLUDED.DanExam3, + DanExam4=EXCLUDED.DanExam4, + DanExam5=EXCLUDED.DanExam5, + DanExam6=EXCLUDED.DanExam6, + DanExam7=EXCLUDED.DanExam7 + "; + cmd.ExecuteNonQuery(); + } + } } } diff --git a/OpenTaiko/src/Helpers/HScenePreset.cs b/OpenTaiko/src/Helpers/HScenePreset.cs index 13240c51..8a2daeb2 100644 --- a/OpenTaiko/src/Helpers/HScenePreset.cs +++ b/OpenTaiko/src/Helpers/HScenePreset.cs @@ -69,10 +69,17 @@ namespace TJAPlayer3 { preset = ((Dictionary)_ps)[TJAPlayer3.stageSongSelect.rChoosenSong.strScenePreset]; } - else + else if (_ps != null + && ((Dictionary)_ps).ContainsKey("")) { preset = ((Dictionary)_ps)[""]; } + else if (_ps != null) + { + var cstps = (Dictionary)_ps; + Random rand = new Random(); + preset = cstps.ElementAt(rand.Next(0, cstps.Count)).Value; + } return preset; } diff --git a/OpenTaiko/src/Stages/07.Game/Taiko/ModIcons.cs b/OpenTaiko/src/Stages/07.Game/Taiko/ModIcons.cs index 61b4ad6d..66921cde 100644 --- a/OpenTaiko/src/Stages/07.Game/Taiko/ModIcons.cs +++ b/OpenTaiko/src/Stages/07.Game/Taiko/ModIcons.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace TJAPlayer3 { @@ -47,6 +48,8 @@ namespace TJAPlayer3 TJAPlayer3.Tx.Mod_None.Opacity = 255; } + #region [Displayables] + static private void tDisplayHSIcon(int x, int y, int player) { // TO DO : Add HS x0.5 icon (_vals == 4) @@ -155,5 +158,27 @@ namespace TJAPlayer3 TJAPlayer3.Tx.Mod_None?.t2D描画(x, y); } + #endregion + + #region [Mod flags] + + static public Int64 tModsToPlayModsFlags(int player) + { + byte[] _flags = new byte[8] { 0, 0, 0, 0, 0, 0, 0, 0 }; + int actual = TJAPlayer3.GetActualPlayer(player); + + _flags[0] = (byte)Math.Min(255, TJAPlayer3.ConfigIni.nScrollSpeed[actual]); + _flags[1] = (byte)TJAPlayer3.ConfigIni.eSTEALTH[actual]; + _flags[2] = (byte)TJAPlayer3.ConfigIni.eRandom[actual]; + _flags[3] = (byte)Math.Min(255, TJAPlayer3.ConfigIni.n演奏速度); + _flags[4] = (byte)TJAPlayer3.ConfigIni.nTimingZones[actual]; + _flags[5] = (byte)TJAPlayer3.ConfigIni.bJust[actual]; + _flags[7] = (byte)TJAPlayer3.ConfigIni.nFunMods[actual]; + + return BitConverter.ToInt64(_flags, 0); + } + + + #endregion } } diff --git a/OpenTaiko/src/Stages/08.Result/CStage結果.cs b/OpenTaiko/src/Stages/08.Result/CStage結果.cs index 233675d3..c4e5340c 100644 --- a/OpenTaiko/src/Stages/08.Result/CStage結果.cs +++ b/OpenTaiko/src/Stages/08.Result/CStage結果.cs @@ -111,10 +111,44 @@ namespace TJAPlayer3 bAddedToRecentlyPlayedSongs = false; try { + /* + * Notes about the difference between Replay - Save statuses and the "Assisted clear" clear status + * + * - The values for replay files are 0 if no status, while for save files they start by -1 + * - The "Assisted clear" status is used on the save files, but NOT on the replay files + * - The "Assisted clear" status is also not used in the coins evaluations + */ int[] ClearStatus_Replay = new int[5] { 0, 0, 0, 0, 0 }; int[] ScoreRank_Replay = new int[5] { 0, 0, 0, 0, 0 }; - { + int[] clearStatuses = + { + -1, + -1, + -1, + -1, + -1 + }; + + int[] scoreRanks = + { + -1, + -1, + -1, + -1, + -1 + }; + + bool[] assistedClear = + { + (TJAPlayer3.stageSongSelect.actPlayOption.tGetModMultiplier(CActPlayOption.EBalancingType.SCORE, false, 0) < 1f), + (TJAPlayer3.stageSongSelect.actPlayOption.tGetModMultiplier(CActPlayOption.EBalancingType.SCORE, false, 1) < 1f), + (TJAPlayer3.stageSongSelect.actPlayOption.tGetModMultiplier(CActPlayOption.EBalancingType.SCORE, false, 2) < 1f), + (TJAPlayer3.stageSongSelect.actPlayOption.tGetModMultiplier(CActPlayOption.EBalancingType.SCORE, false, 3) < 1f), + (TJAPlayer3.stageSongSelect.actPlayOption.tGetModMultiplier(CActPlayOption.EBalancingType.SCORE, false, 4) < 1f) + }; + + { #region [ 初期化 ] //--------------------- this.eフェードアウト完了時の戻り値 = E戻り値.継続; @@ -188,7 +222,11 @@ namespace TJAPlayer3 this.nクリア[p] = 2; if (ccf.nGood == 0) this.nクリア[p] = 3; } - } + + if (assistedClear[p]) clearStatuses[p] = 0; + else clearStatuses[p] = this.nクリア[p]; + + } if ((int)TJAPlayer3.stage演奏ドラム画面.actScore.Get(EInstrumentPad.DRUMS, p) < 500000) { @@ -206,7 +244,9 @@ namespace TJAPlayer3 } } } - } + scoreRanks[p] = this.nスコアランク[p] - 1; + + } } @@ -339,7 +379,11 @@ namespace TJAPlayer3 if (this.st演奏記録.Drums.nGreat数 == 0) clearValue += 2; } - } + + if (assistedClear[0]) clearStatuses[0] = (examStatus == Exam.Status.Better_Success) ? 1 : 0; + else clearStatuses[0] = clearValue + 1; + + } if (isAutoDisabled(0)) { @@ -423,6 +467,8 @@ namespace TJAPlayer3 int tmpClear = GetTowerScoreRank(); + if (tmpClear != 0) clearStatuses[0] = assistedClear[0] ? 0 : tmpClear; + if (isAutoDisabled(0)) { ini[0].stセクション[0].nクリア[0] = Math.Max(ini[0].stセクション[0].nクリア[0], tmpClear); @@ -546,7 +592,7 @@ namespace TJAPlayer3 return chara.GetEffectCoinMultiplier() * puchichara.GetEffectCoinMultiplier(); } - if (TJAPlayer3.stageSongSelect.nChoosenSongDifficulty[0] == (int)Difficulty.Tower) + if (TJAPlayer3.stageSongSelect.nChoosenSongDifficulty[0] == (int)Difficulty.Tower) { diffModifier = 3; @@ -563,13 +609,15 @@ namespace TJAPlayer3 #region [Clear modifier] int clearModifier = 0; - - if (this.st演奏記録.Drums.nMiss数 == 0) - { - clearModifier = (int)(5 * lengthBonus); - if (this.st演奏記録.Drums.nGreat数 == 0) - clearModifier = (int)(12 * lengthBonus); - } + + if (this.st演奏記録.Drums.nMiss数 == 0) + { + clearModifier = (int)(5 * lengthBonus); + if (this.st演奏記録.Drums.nGreat数 == 0) + { + clearModifier = (int)(12 * lengthBonus); + } + } #endregion @@ -722,6 +770,9 @@ namespace TJAPlayer3 _cs = 2; } } + + // Unsafe function, it is the only appropriate place to call it + HDatabaseHelpers.RegisterPlay(i, clearStatuses[i], scoreRanks[i]); if (TJAPlayer3.stageSongSelect.actPlayOption.tGetModMultiplier(CActPlayOption.EBalancingType.SCORE, false, i) == 1f) _sf.tUpdateSongClearStatus(TJAPlayer3.stageSongSelect.rChoosenSong, _cs, TJAPlayer3.stageSongSelect.nChoosenSongDifficulty[i]);