diff --git a/game/config.zig b/game/config.zig index de55392..ce69643 100644 --- a/game/config.zig +++ b/game/config.zig @@ -7,6 +7,7 @@ const Self = @This(); volume: f64, window_scale: f32, display_romaji: bool, +vsync: bool, pub fn readConfig() !Self { var file = try std.fs.cwd().openFile("UTyping_config.txt", .{}); @@ -18,6 +19,7 @@ pub fn readConfig() !Self { .volume = 0.25, .window_scale = 1.0, .display_romaji = true, + .vsync = false, }; while (try ini_reader.next()) |item| { @@ -28,6 +30,9 @@ pub fn readConfig() !Self { self.window_scale = try std.fmt.parseFloat(f32, item.value); } else if (std.mem.eql(u8, item.key, "DisplayRomaji")) { self.display_romaji = try std.fmt.parseInt(u1, item.value, 2) != 0; + } else if (std.mem.eql(u8, item.key, "WaitVSync")) { + //Bit of an ugly hack, but sure! + self.vsync = std.ascii.eqlIgnoreCase(item.value, "true"); } else { std.debug.print("Unknown config option \"{s}\" with value \"{s}\"\n", .{ item.key, item.value }); } diff --git a/game/fontstash.zig b/game/fontstash.zig index 04b8e5a..9206cf8 100644 --- a/game/fontstash.zig +++ b/game/fontstash.zig @@ -20,6 +20,14 @@ pub const Normal: Fontstash.State = .{ }, }; +pub const Ranking: Fontstash.State = .{ + .font = Mincho, + .size = 16, + .alignment = .{ + .vertical = .top, + }, +}; + gfx: *Gfx, renderer: Renderer, context: *Fontstash, diff --git a/game/gfx.zig b/game/gfx.zig index e6460ef..ab7f18a 100644 --- a/game/gfx.zig +++ b/game/gfx.zig @@ -4,6 +4,8 @@ const c = @import("main.zig").c; const zmath = @import("zmath"); const img = @import("zigimg"); +const Config = @import("config.zig"); + const Self = @This(); pub const Vector2 = @Vector(2, f32); @@ -15,6 +17,7 @@ pub const ColorF = @Vector(4, f32); pub const ColorB = @Vector(4, u8); ///A rectangle with its bounds specified as u32 pub const RectU = @Vector(4, u32); +pub const RectF = @Vector(4, f32); pub const Vector2One: Vector2 = @splat(@as(f32, 1)); pub const Vector2Zero: Vector2 = @splat(@as(f32, 0)); @@ -130,9 +133,9 @@ const PresentMode = enum(c_uint) { mailbox = 3, }; -pub fn init(window: *c.SDL_Window, scale: f32) !Self { +pub fn init(window: *c.SDL_Window, config: Config) !Self { var self: Self = Self{ - .scale = scale, + .scale = config.window_scale, }; //Update the viewport @@ -165,7 +168,7 @@ pub fn init(window: *c.SDL_Window, scale: f32) !Self { var modes_to_try: []const PresentMode = &.{ .mailbox, .fifo, .immediate }; //If we are on an iGPU, lets prefer standard vsync over mailbox - if (properties.adapterType == c.WGPUAdapterType_IntegratedGPU) { + if (config.vsync) { modes_to_try = &.{ .fifo, .mailbox, .immediate }; } diff --git a/game/main.zig b/game/main.zig index 8976096..2cf6b7e 100644 --- a/game/main.zig +++ b/game/main.zig @@ -111,7 +111,7 @@ fn runGame() !void { try bass.setConfig(.global_stream_volume, @intFromFloat(state.config.volume * 10000)); //Initialize our graphics - var gfx: Gfx = try Gfx.init(window, state.config.window_scale); + var gfx: Gfx = try Gfx.init(window, state.config); defer gfx.deinit(); const imgui_context = c.igCreateContext(null); diff --git a/game/music.zig b/game/music.zig index fef7a97..cf2cdf4 100644 --- a/game/music.zig +++ b/game/music.zig @@ -152,7 +152,22 @@ 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(); - const ranking_path = try path.realpathAlloc(allocator, self.ranking_file_name); + const ranking_path = path.realpathAlloc(allocator, self.ranking_file_name) catch |err| blk: { + //If we got a FileNotFound error, write out a new blank ranking file + if (err == std.fs.Dir.OpenError.FileNotFound) { + const ranking = Ranking.init(); + + var new_ranking_file = try path.createFile(self.ranking_file_name, .{}); + + try ranking.writeRanking(new_ranking_file.writer()); + + new_ranking_file.close(); + + break :blk try path.realpathAlloc(allocator, self.ranking_file_name); + } else { + return err; + } + }; defer allocator.free(ranking_path); var ranking_file = try std.fs.openFileAbsolute(ranking_path, .{}); @@ -332,20 +347,6 @@ pub fn draw( _ = try render_state.fontstash.drawText(.{ title_x, title_y + y }, self.title, state); } -pub fn drawRanking( - self: Self, - render_state: Screen.RenderState, - pos: Gfx.Vector2, - ranking_pos: isize, - rank_len: usize, -) !void { - _ = self; - _ = rank_len; - _ = ranking_pos; - _ = pos; - _ = render_state; -} - pub fn drawComment(self: Self, render_state: Screen.RenderState, pos: Gfx.Vector2) !void { for (self.comment, 0..) |comment, i| { _ = try render_state.fontstash.drawText( diff --git a/game/ranking.zig b/game/ranking.zig index d8a1730..9560caa 100644 --- a/game/ranking.zig +++ b/game/ranking.zig @@ -29,6 +29,20 @@ pub const UTypingDate = packed struct(i32) { year: u16, }; +pub fn init() Self { + var self = Self{ + .play_time = 0, + .play_count = 0, + .achievement = .no_data, + .changed = false, + .scores = undefined, + }; + + @memset(&self.scores, Score.init()); + + return self; +} + /// Gets the current date, in the UTyping date format pub fn getUTypingDate() UTypingDate { //Get the current timestamp @@ -53,19 +67,19 @@ pub fn draw( pos: Gfx.Vector2, rank_begin: usize, rank_len: usize, - comptime font: []const u8, ) !void { - _ = render_state; for (rank_begin..(rank_begin + rank_len)) |i| { //Break out if we are drawing more than the max amount of storable ranks if (i >= ranking_len) { break; } - try self.scores[i].draw(pos + Gfx.Vector2{ 0, 48 * i }, i + 1, font); + try self.scores[i].draw(render_state, pos + Gfx.Vector2{ 0, @floatFromInt(48 * i) }, i + 1); } } +const ranking_file_version = 5; + pub fn readRanking(file: std.fs.File) !Self { var ranking = Self{ .achievement = .no_data, @@ -89,8 +103,6 @@ pub fn readRanking(file: std.fs.File) !Self { 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; @@ -108,7 +120,7 @@ pub fn readRanking(file: std.fs.File) !Self { return error.UnknownRankingVersion; } - //In version 4, achivement was added to the score data34 + //In version 4, achivement was added to the score data if (version >= 4) { ranking.achievement = try reader.readEnum(RankingLevel, .little); } @@ -144,6 +156,17 @@ pub fn readRanking(file: std.fs.File) !Self { return ranking; } +pub fn writeRanking(self: Self, writer: std.fs.File.Writer) !void { + try writer.writeInt(i32, ranking_file_version, .little); + try writer.writeInt(i32, @intFromEnum(self.achievement), .little); + try writer.writeInt(i32, self.play_count, .little); + try writer.writeInt(i32, self.play_time, .little); + try writer.writeInt(i32, self.scores.len, .little); + for (&self.scores) |score| { + try score.write(writer); + } +} + pub const RankingLevel = enum(i32) { no_data = 0, failed = 0x0100, @@ -194,7 +217,9 @@ pub const Score = struct { .month = 0, .year = 0, }, - .challenge = .{}, + .challenge = .{ + .speed = 0, + }, }; //Zero-init the score to all zeroes @@ -210,13 +235,72 @@ pub const Score = struct { render_state: RenderState, pos: Gfx.Vector2, n: usize, - comptime font: []const u8, ) !void { - _ = font; - _ = n; - _ = pos; - _ = render_state; - _ = self; + var buf: [256]u8 = undefined; + var stream = std.io.fixedBufferStream(&buf); + const writer = stream.writer(); + + const color = Gfx.ColorF{ 1, 1, 1, 1 }; + { + stream.pos = 0; + try formatOrdinal(writer, n); + try std.fmt.format(writer, ": {s} {d:0>8} 点( {d:0>7} + {d:0>7} ),", .{ + std.mem.sliceTo(&self.name, 0), + @as(u32, @intCast(self.score)), + @as(u32, @intCast(self.score_accuracy)), + @as(u32, @intCast(self.score_typing)), + }); + + var state = Fontstash.Ranking; + state.color = color; + + _ = try render_state.fontstash.drawText(pos + Gfx.Vector2{ 40, 6 }, buf[0..stream.pos], state); + } + { + stream.pos = 0; + if (self.date.year == 0) { + try std.fmt.format(writer, "----/--/--", .{}); + } else { + try std.fmt.format(writer, "{d:0>4}/{d:0>2}/{d:0>2}", .{ + @as(u32, @intCast(self.date.year)), + @as(u32, @intCast(self.date.month)), + @as(u32, @intCast(self.date.day)), + }); + } + + var state = Fontstash.Ranking; + state.color = color; + state.alignment.horizontal = .right; + + _ = try render_state.fontstash.drawText(pos + Gfx.Vector2{ Screen.display_width - 40, 6 }, buf[0..stream.pos], state); + } + { + stream.pos = 0; + try self.challenge.format({}, {}, writer); + if (self.combo_max >= 0) { + try std.fmt.format(writer, ", 最大 {d:0>4} コンボ, ({d:0>4}/{d:0>4}/{d:0>4}/{d:0>4}/{d:0>4})", .{ + @as(u32, @intCast(self.combo_max)), + @as(u32, @intCast(self.count[0])), + @as(u32, @intCast(self.count[1])), + @as(u32, @intCast(self.count[2])), + @as(u32, @intCast(self.count[3])), + @as(u32, @intCast(self.count[4])), + }); + } else { + try std.fmt.format(writer, ", フル コンボ, ({d:0>4}/{d:0>4}/{d:0>4}/{d:0>4}/{d:0>4})", .{ + @as(u32, @intCast(self.count[0])), + @as(u32, @intCast(self.count[1])), + @as(u32, @intCast(self.count[2])), + @as(u32, @intCast(self.count[3])), + @as(u32, @intCast(self.count[4])), + }); + } + var state = Fontstash.Ranking; + state.color = color; + state.alignment.horizontal = .right; + + _ = try render_state.fontstash.drawText(pos + Gfx.Vector2{ Screen.display_width - 40, 48 - 6 - 16 }, buf[0..stream.pos], state); + } } pub fn getLevel(self: Score) RankingLevel { @@ -226,8 +310,10 @@ pub const Score = struct { return .perfect; } + const challenge_num = 7; + pub fn read(reader: anytype, version: i32) !Score { - var score = init(); + var score = Score.init(); //In version 1, the score name length was bumped from 8 to 16 if (version >= 1) { @@ -254,7 +340,6 @@ pub const Score = struct { //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 @@ -291,4 +376,33 @@ pub const Score = struct { return score; } + + pub fn write(self: Score, writer: std.fs.File.Writer) !void { + try writer.writeAll(&self.name); + try writer.writeInt(i32, self.score, .little); + try writer.writeInt(i32, self.score_accuracy, .little); + try writer.writeInt(i32, self.score_typing, .little); + for (&self.count) |count| { + try writer.writeInt(i32, count, .little); + } + try writer.writeInt(i32, self.count_all, .little); + try writer.writeInt(i32, self.combo_max, .little); + + try writer.writeByte(@intFromBool(self.challenge.hidden)); + try writer.writeByte(@intFromBool(self.challenge.sudden)); + try writer.writeByte(@intFromBool(self.challenge.stealth)); + try writer.writeByte(@intFromBool(self.challenge.lyrics_stealth)); + try writer.writeByte(@intFromBool(self.challenge.sin)); + try writer.writeByte(@intFromBool(self.challenge.cos)); + try writer.writeByte(@intFromBool(self.challenge.tan)); + + for (challenge_num..16) |_| { + try writer.writeByte(0); + } + + try writer.writeInt(i32, @intFromFloat(self.challenge.speed * 10), .little); + try writer.writeInt(i32, self.challenge.key, .little); + + try writer.writeInt(i32, @bitCast(self.date), .little); + } }; diff --git a/game/screens/song_select.zig b/game/screens/song_select.zig index 32e4447..1df845b 100644 --- a/game/screens/song_select.zig +++ b/game/screens/song_select.zig @@ -860,15 +860,23 @@ pub fn renderScreen(self: *Screen, render_state: RenderState) anyerror!void { fn drawMainRanking(render_state: Screen.RenderState, music: Music, ranking_pos: isize, pos: Gfx.Vector2, height: f32) !void { const yMin = @max(pos[1], 0); - _ = yMin; const yMax = @min(pos[1] + height, 360); - _ = yMax; - // TODO: debug why scissors are borked :( - // try render_state.renderer.setScissor(.{ 10, yMin, Screen.display_width - 20, yMax - yMin }); + var scissor: Gfx.RectF = .{ 10, yMin, Screen.display_width - 20, yMax - yMin }; + scissor *= @splat(render_state.gfx.scale); + + const scissor_u: Gfx.RectU = .{ + @intFromFloat(@max(0, scissor[0])), + @intFromFloat(@max(0, scissor[1])), + @intFromFloat(@max(0, scissor[2])), + @intFromFloat(@max(0, scissor[3])), + }; + + try render_state.renderer.setScissor(scissor_u); + try render_state.fontstash.renderer.setScissor(scissor_u); if (ranking_pos >= 0) { - try music.drawRanking(render_state, .{ pos[0], pos[1] + 6 }, ranking_pos, ranking_draw_len); + try music.ranking.draw(render_state, .{ pos[0], pos[1] + 6 }, @intCast(ranking_pos), ranking_draw_len); } else { try music.drawComment(render_state, .{ pos[0], pos[1] + 6 }); try music.drawPlayData(render_state, .{ pos[0], pos[1] + 6 + h_comment + 4 });