From b4779d5b408004b0dab862de310fd0ac4c19d37a Mon Sep 17 00:00:00 2001 From: Beyley Thomas Date: Thu, 30 Nov 2023 13:14:13 -0800 Subject: [PATCH] Implement reading of ranking files --- game/challenge.zig | 20 ++-- game/music.zig | 11 ++ game/ranking.zig | 244 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 264 insertions(+), 11 deletions(-) create mode 100644 game/ranking.zig diff --git a/game/challenge.zig b/game/challenge.zig index c987945..95894fd 100644 --- a/game/challenge.zig +++ b/game/challenge.zig @@ -21,20 +21,18 @@ pub const ChallengeInfo = struct { /// this is used to fix audio delay caused by the OS, hardware, and poor map timing audio_offset: i16 = 0, - /// Whether or not the challenge is valid - valid: bool = true, - const Self = @This(); - pub fn format(self: Self, render_state: *RenderState, writer: anytype) !void { - _ = render_state; + pub fn format(self: Self, actual_fmt: anytype, options: anytype, writer: anytype) !void { + _ = options; + _ = actual_fmt; //If its not valid, - if (!self.valid) { - //Write some hyphens - try writer.writeAll("----"); - //Break out - return; - } + // if (!self.valid) { + // //Write some hyphens + // try writer.writeAll("----"); + // //Break out + // return; + // } //Write the speed try writer.writeByte('x'); diff --git a/game/music.zig b/game/music.zig index d0e7f6f..469fba3 100644 --- a/game/music.zig +++ b/game/music.zig @@ -2,6 +2,7 @@ const std = @import("std"); const IConv = @import("iconv.zig"); const Fumen = @import("fumen.zig"); +const Ranking = @import("ranking.zig"); const Gfx = @import("gfx.zig"); const Screen = @import("screen.zig"); @@ -21,6 +22,7 @@ folder_path: []const u8, sort_idx: usize, fumen: *Fumen, +ranking: Ranking, allocator: std.mem.Allocator, @@ -150,6 +152,15 @@ pub fn readFromFile(allocator: std.mem.Allocator, path: std.fs.Dir, file: std.fs self.fumen.* = try Fumen.readFromFile(allocator, fumen_file, path); errdefer self.fumen.deinit(); + var ranking_path = try path.realpathAlloc(allocator, self.ranking_file_name); + defer allocator.free(ranking_path); + + var ranking_file = try std.fs.openFileAbsolute(ranking_path, .{}); + defer ranking_file.close(); + + self.ranking = try Ranking.readRanking(allocator, ranking_file); + errdefer self.ranking.deinit(); + return self; } diff --git a/game/ranking.zig b/game/ranking.zig new file mode 100644 index 0000000..86567e7 --- /dev/null +++ b/game/ranking.zig @@ -0,0 +1,244 @@ +const std = @import("std"); + +const Challenge = @import("challenge.zig").ChallengeInfo; + +const Self = @This(); + +/// The maximum length for a UTyping score name +const name_len = 16; + +/// The maximum number of rankings stored +const ranking_len = 20; + +/// Whether or not the scores have changed, and they should be re-written to the file +changed: bool = false, +scores: [ranking_len]Score, +achievement: RankingLevel, +play_count: i32, +play_time: i32, + +pub const UTypingDate = packed struct(i32) { + day: u8, + month: u8, + year: u16, +}; + +/// Gets an integer representing the current date +pub fn getUTypingDate() UTypingDate { + //Get the current timestamp + var epoch_seconds = std.time.epoch.EpochSeconds{ + .secs = @intCast(std.time.timestamp()), + }; + //Get the year and day + var year_day = epoch_seconds.getEpochDay().calculateYearDay(); + //Get the year and month + var month_and_day = year_day.calculateMonthDay(); + + return .{ + .day = month_and_day.day_index, + .month = @intFromEnum(month_and_day.month), + .year = year_day.year, + }; +} + +pub fn readRanking(allocator: std.mem.Allocator, file: std.fs.File) !Self { + _ = allocator; + var ranking = Self{ + .achievement = .no_data, + .play_count = 0, + .play_time = 0, + .changed = false, + //set scores to undefined, as it is init later + .scores = .{comptime Score.init()} ** ranking_len, + }; + //Set all of the scores to the default state + @memset(&ranking.scores, Score.init()); + + var buffered_reader = std.io.bufferedReader(file.reader()); + var reader = buffered_reader.reader(); + + var version: i32 = reader.readInt(i32, .little) catch |err| { + if (err == std.fs.File.Reader.NoEofError.EndOfStream) { + return ranking; + } + + return err; + }; + + const ranking_file_version = 5; + + //If the first byte of the version is greater than 32, than assume version is 0 + if ((version & 0xff) > ' ') { + version = 0; + //Seek back to the start + try file.seekTo(0); + + //Reset the buffered reader + buffered_reader = std.io.bufferedReader(file.reader()); + reader = buffered_reader.reader(); + } + + //If we found an invalid ranking version, throw an error + if (version > ranking_file_version or version < 0) { + std.debug.print("found unknown ranking version {d}\n", .{version}); + return error.UnknownRankingVersion; + } + + //In version 4, achivement was added to the score data34 + if (version >= 4) { + ranking.achievement = try reader.readEnum(RankingLevel, .little); + } + + //In version 3, play count and play time was added + if (version >= 3) { + ranking.play_count = try reader.readInt(i32, .little); + ranking.play_time = try reader.readInt(i32, .little); + } + + //In version 5, we now only store the exact number of scores, rather than all ranking_len scores. + var ranking_size: usize = if (version >= 5) @intCast(try reader.readInt(i32, .little)) else ranking_len; + + //Iterate through all the rankings and read the scores + for (0..ranking_size) |i| { + ranking.scores[i] = try Score.read(reader, version); + } + + //In version 4, achivement was added to the score data + if (version < 4) { + for (&ranking.scores) |score| { + const level = score.getLevel(); + //If this score is better than the currently stored achivement + if (@intFromEnum(level) > @intFromEnum(ranking.achievement)) { + //Store that achivement + ranking.achievement = level; + } + } + } + + try file.seekTo(0); + + return ranking; +} + +pub const RankingLevel = enum(i32) { + no_data = 0, + failed = 0x0100, + red_zone = 0x0200, + yellow_zone = 0x0240, + blue_zone = 0x0280, + clear = 0x0300, + full_combo = 0x0500, + full_good = 0x0600, + perfect = 0x0700, +}; + +pub const Score = struct { + name: [name_len]u8, + score: i32, + score_accuracy: i32, + score_typing: i32, + ///The counts of the judgements, in order of: Excellent, Good, Fair, Poor, Pass + count: [5]i32, + count_all: i32, + combo_max: i32, + challenge: Challenge, + date: UTypingDate, + + pub fn init() Score { + var score = Score{ + .name = undefined, + .score = 0, + .score_accuracy = 0, + .score_typing = 0, + .count = .{ 0, 0, 0, 0, 0 }, + .count_all = 0, + .combo_max = 0, + .date = .{ + .day = 0, + .month = 0, + .year = 0, + }, + .challenge = .{}, + }; + + //Zero-init the score to all zeroes + @memset(&score.name, 0); + //Set the first char to `_` + score.name[0] = '_'; + + return score; + } + + pub fn getLevel(self: Score) RankingLevel { + if (self.combo_max != -1) return .no_data; + if (self.count[2] != 0) return .full_combo; + if (self.count[1] != 0) return .full_good; + return .perfect; + } + + pub fn read(reader: anytype, version: i32) !Score { + var score = init(); + + //In version 1, the score name length was bumped from 8 to 16 + if (version >= 1) { + //Read the full name_len bytes for the name + _ = try reader.readNoEof(&score.name); + } else { + //Read only 8 bytes, as names were only 8 bytes in size in version 1 + _ = try reader.readNoEof(score.name[0..8]); + score.name[8] = 0; + } + //Read the combined score + score.score = try reader.readInt(i32, .little); + //Read the accuracy score + score.score_accuracy = try reader.readInt(i32, .little); + //Read the typing score + score.score_typing = try reader.readInt(i32, .little); + //Iterate over all the count parameters + for (&score.count) |*count| { + //Set them to the read value + count.* = try reader.readInt(i32, .little); + } + //Read the total count + score.count_all = try reader.readInt(i32, .little); + //Read the max combo + score.combo_max = try reader.readInt(i32, .little); + + const challenge_num = 7; + //In version 1, challenges were added + if (version >= 1) { + //Iterate over all known challenges + for (0..challenge_num) |i| { + //If the challenge is enabled + if (try reader.readByte() != 0) + //Enable the associated challenge + switch (i) { + 0 => score.challenge.hidden = true, + 1 => score.challenge.sudden = true, + 2 => score.challenge.stealth = true, + 3 => score.challenge.lyrics_stealth = true, + 4 => score.challenge.sin = true, + 5 => score.challenge.cos = true, + 6 => score.challenge.tan = true, + else => @panic("Unhandled challenge int"), + }; + } + //Ignore all the rest of the bytes up to 16 (left for future use I assume) + for (challenge_num..16) |_| { + _ = try reader.readByte(); + } + //Read the speed + score.challenge.speed = @as(f64, @floatFromInt(try reader.readInt(i32, .little))) / 10.0; + //Read the key + score.challenge.key = try reader.readInt(i32, .little); + } + + //In version 2, dates were added to the scores + if (version >= 2) { + //Read the date + score.date = @bitCast(try reader.readInt(i32, .little)); + } + + return score; + } +};