From b39e540c6952d1b06747cc9ab67359f88dca10f6 Mon Sep 17 00:00:00 2001 From: jremitz Date: Fri, 17 Apr 2026 07:03:46 -0500 Subject: [PATCH 1/4] fix: Sync cumulative game clock with period clock display (#16) Truncate current-period elapsed tenths to whole-second boundaries so the cumulative clock uses the same rounding as the displayed period clock, eliminating the 1-second drift caused by independent truncation. Co-Authored-By: Claude --- CHANGELOG.md | 5 +++ CMakeLists.txt | 2 +- src/scoreboard-core.c | 8 +++- tests/test-scoreboard-core.c | 85 ++++++++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39fe11e..b3ca356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to Streamn Scoreboard will be documented in this file. ## [Unreleased] +## [0.7.1] - 2026-04-17 + +### Fixed +- Cumulative game clock no longer drifts 1 second from the period clock — both clocks now truncate sub-second time consistently, so they always agree (#16) + ## [0.7.0] - 2026-04-14 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index ef98730..a7ae4b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.21) -project(streamn_obs_scoreboard VERSION 0.7.0 LANGUAGES C CXX) +project(streamn_obs_scoreboard VERSION 0.7.1 LANGUAGES C CXX) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) diff --git a/src/scoreboard-core.c b/src/scoreboard-core.c index b0767a7..2662035 100644 --- a/src/scoreboard-core.c +++ b/src/scoreboard-core.c @@ -531,10 +531,14 @@ int scoreboard_get_period_length(void) static int current_period_elapsed_tenths(void) { + int elapsed; if (g_state.clock_direction == SCOREBOARD_CLOCK_COUNT_DOWN) - return g_state.period_length * 10 - g_state.clock_tenths; + elapsed = g_state.period_length * 10 - g_state.clock_tenths; else - return g_state.clock_tenths; + elapsed = g_state.clock_tenths; + /* Truncate to whole seconds so cumulative clock matches the + displayed period clock (both truncate tenths the same way). */ + return (elapsed / 10) * 10; } void scoreboard_set_game_clock_enabled(bool enabled) diff --git a/tests/test-scoreboard-core.c b/tests/test-scoreboard-core.c index 577ea1c..2b7aec2 100644 --- a/tests/test-scoreboard-core.c +++ b/tests/test-scoreboard-core.c @@ -995,6 +995,89 @@ static void test_game_clock_format_hmmss(void) assert(strcmp(buf, "1:01:33") == 0); } +static void test_game_clock_subsecond_sync(void) +{ + /* Cumulative clock must agree with the period clock at the + whole-second level. Before the fix, independent truncation of + tenths could cause a 1-second discrepancy (GitHub issue #16). */ + scoreboard_reset_state_for_tests(); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(1200); /* 20:00 countdown */ + scoreboard_clock_reset(); + scoreboard_clock_start(); + + /* Tick 1 tenth past a second boundary (clock_tenths goes from + 12000 to 11999). Period clock: 11999/10 = 1199s = 19:59. + Elapsed tenths = 12000 - 11999 = 1 → old code: 1/10 = 0s. + So old cumulative = 0:00 while period clock says 19:59. + Fixed code truncates elapsed to 0 tenths, giving 0:00 — which + still adds up (0:00 + 19:59 = 19:59 → first real second + hasn't completed yet, both agree the first full second hasn't + elapsed). */ + scoreboard_clock_tick(1); + assert(scoreboard_game_clock_get_tenths() == 0); + + /* Now tick to 9 tenths into a second (clock_tenths = 11991). + Period clock: 11991/10 = 1199s = 19:59. + Elapsed tenths = 12000 - 11991 = 9. + Fixed code: (9/10)*10 = 0. Cumulative = 0:00. + Period says 19:59; 0:00 + 19:59 = 19:59 — consistent. */ + scoreboard_clock_tick(8); /* total ticked = 9 */ + assert(scoreboard_game_clock_get_tenths() == 0); + + /* Tick one more to complete the first full second (10 tenths). + clock_tenths = 11990. Period clock: 11990/10 = 1199s = 19:59. + Elapsed = 12000 - 11990 = 10. (10/10)*10 = 10. + Cumulative = 0:01. Period says 19:59; 0:01 + 19:59 = 20:00 ✓ */ + scoreboard_clock_tick(1); /* total ticked = 10 */ + assert(scoreboard_game_clock_get_tenths() == 10); + + /* Tick 11 more to land mid-second (clock_tenths = 11979). + Period clock: 11979/10 = 1197s = 19:57. + Elapsed = 12000 - 11979 = 21. (21/10)*10 = 20. + Cumulative = 0:02. 0:02 + 19:57 = 19:59 — wait, that's + only 19:59 not 20:00, but that's correct because both clocks + have dropped the sub-second remainder consistently. + Key invariant: period_displayed + cumulative_displayed >= + period_length - 1 (at most 1s truncation total). */ + scoreboard_clock_tick(11); /* total ticked = 21 */ + assert(scoreboard_game_clock_get_tenths() == 20); + + /* Verify formatted strings agree: period shows 19:57, + cumulative shows 0:02. The sum (19:59) is within 1s of + period_length (20:00) due to shared truncation — but critically + they no longer disagree with each other by 1s. */ + char clock_buf[32], cum_buf[32]; + scoreboard_clock_format(clock_buf, sizeof(clock_buf)); + scoreboard_game_clock_format(cum_buf, sizeof(cum_buf)); + assert(strcmp(clock_buf, "19:57") == 0); + assert(strcmp(cum_buf, "0:02") == 0); +} + +static void test_game_clock_subsecond_sync_across_periods(void) +{ + /* Ensure the fix holds across period boundaries when the clock + stops mid-second at period advance. */ + scoreboard_reset_state_for_tests(); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(60); /* 1:00 periods */ + scoreboard_clock_reset(); + scoreboard_clock_start(); + + /* Tick 5 tenths into second 1 then advance period. + Elapsed = 600 - 595 = 5 tenths → truncated to 0. + accumulated += 0 after advance. */ + scoreboard_clock_tick(5); + scoreboard_period_advance(); + /* accumulated = 0, period 2 clock reset */ + + /* In period 2, tick a full 10 tenths (1 second). */ + scoreboard_clock_start(); + scoreboard_clock_tick(10); + /* Current elapsed = 600 - 590 = 10 → (10/10)*10 = 10. */ + assert(scoreboard_game_clock_get_tenths() == 10); +} + static void test_game_clock_format_mmss_over_hour(void) { /* MM:SS format — minutes exceed 59 */ @@ -1102,6 +1185,8 @@ int main(void) test_game_clock_format_null(); test_game_clock_format_disabled(); test_game_clock_format_hmmss(); + test_game_clock_subsecond_sync(); + test_game_clock_subsecond_sync_across_periods(); test_game_clock_format_mmss_over_hour(); printf("All scoreboard-core clock/period tests passed.\n"); From 3b71ac6af8e37f84eed03af23076de9beec59dc6 Mon Sep 17 00:00:00 2001 From: jremitz Date: Fri, 17 Apr 2026 07:19:07 -0500 Subject: [PATCH 2/4] fix: Derive cumulative elapsed from displayed period clock seconds The first approach (truncating raw elapsed tenths) still allowed a 1-second discrepancy because the period clock and cumulative clock truncated independently. Now cumulative elapsed is computed as (period_length - displayed_seconds), guaranteeing the invariant displayed_period + displayed_cumulative == period_length at every tick. Co-Authored-By: Claude --- src/scoreboard-core.c | 14 +++--- tests/test-scoreboard-core.c | 86 ++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 55 deletions(-) diff --git a/src/scoreboard-core.c b/src/scoreboard-core.c index 2662035..976587d 100644 --- a/src/scoreboard-core.c +++ b/src/scoreboard-core.c @@ -531,14 +531,14 @@ int scoreboard_get_period_length(void) static int current_period_elapsed_tenths(void) { - int elapsed; + /* Derive elapsed from the displayed (truncated-to-seconds) period + clock so that displayed_period + cumulative == period_length + at every instant. Without this, independent truncation of raw + tenths causes a 1-second drift (GitHub #16). */ + int displayed_seconds = g_state.clock_tenths / 10; if (g_state.clock_direction == SCOREBOARD_CLOCK_COUNT_DOWN) - elapsed = g_state.period_length * 10 - g_state.clock_tenths; - else - elapsed = g_state.clock_tenths; - /* Truncate to whole seconds so cumulative clock matches the - displayed period clock (both truncate tenths the same way). */ - return (elapsed / 10) * 10; + return (g_state.period_length - displayed_seconds) * 10; + return displayed_seconds * 10; } void scoreboard_set_game_clock_enabled(bool enabled) diff --git a/tests/test-scoreboard-core.c b/tests/test-scoreboard-core.c index 2b7aec2..aa29420 100644 --- a/tests/test-scoreboard-core.c +++ b/tests/test-scoreboard-core.c @@ -997,85 +997,75 @@ static void test_game_clock_format_hmmss(void) static void test_game_clock_subsecond_sync(void) { - /* Cumulative clock must agree with the period clock at the - whole-second level. Before the fix, independent truncation of - tenths could cause a 1-second discrepancy (GitHub issue #16). */ + /* Cumulative clock must always satisfy: + displayed_period + displayed_cumulative == period_length + Before the fix, independent truncation of raw tenths caused a + 1-second discrepancy (GitHub issue #16). The fix derives + elapsed from the displayed period-clock seconds. */ scoreboard_reset_state_for_tests(); scoreboard_set_game_clock_enabled(true); scoreboard_set_period_length(1200); /* 20:00 countdown */ scoreboard_clock_reset(); scoreboard_clock_start(); - /* Tick 1 tenth past a second boundary (clock_tenths goes from - 12000 to 11999). Period clock: 11999/10 = 1199s = 19:59. - Elapsed tenths = 12000 - 11999 = 1 → old code: 1/10 = 0s. - So old cumulative = 0:00 while period clock says 19:59. - Fixed code truncates elapsed to 0 tenths, giving 0:00 — which - still adds up (0:00 + 19:59 = 19:59 → first real second - hasn't completed yet, both agree the first full second hasn't - elapsed). */ + /* Tick 1 tenth (clock_tenths 12000 → 11999). + Period displays 11999/10 = 1199s = 19:59. + Elapsed = (1200 - 1199) * 10 = 10 → cumulative 0:01. + 19:59 + 0:01 = 20:00 ✓ */ scoreboard_clock_tick(1); - assert(scoreboard_game_clock_get_tenths() == 0); + assert(scoreboard_game_clock_get_tenths() == 10); - /* Now tick to 9 tenths into a second (clock_tenths = 11991). - Period clock: 11991/10 = 1199s = 19:59. - Elapsed tenths = 12000 - 11991 = 9. - Fixed code: (9/10)*10 = 0. Cumulative = 0:00. - Period says 19:59; 0:00 + 19:59 = 19:59 — consistent. */ - scoreboard_clock_tick(8); /* total ticked = 9 */ - assert(scoreboard_game_clock_get_tenths() == 0); + /* Tick 8 more (total 9, clock_tenths = 11991). + Period still 11991/10 = 1199s = 19:59. + Cumulative still 0:01. 19:59 + 0:01 = 20:00 ✓ */ + scoreboard_clock_tick(8); + assert(scoreboard_game_clock_get_tenths() == 10); - /* Tick one more to complete the first full second (10 tenths). - clock_tenths = 11990. Period clock: 11990/10 = 1199s = 19:59. - Elapsed = 12000 - 11990 = 10. (10/10)*10 = 10. - Cumulative = 0:01. Period says 19:59; 0:01 + 19:59 = 20:00 ✓ */ - scoreboard_clock_tick(1); /* total ticked = 10 */ + /* Tick 1 more to cross the second boundary (total 10, + clock_tenths = 11990). Period 11990/10 = 1199s = 19:59. + Still 0:01. 19:59 + 0:01 = 20:00 ✓ */ + scoreboard_clock_tick(1); assert(scoreboard_game_clock_get_tenths() == 10); - /* Tick 11 more to land mid-second (clock_tenths = 11979). - Period clock: 11979/10 = 1197s = 19:57. - Elapsed = 12000 - 11979 = 21. (21/10)*10 = 20. - Cumulative = 0:02. 0:02 + 19:57 = 19:59 — wait, that's - only 19:59 not 20:00, but that's correct because both clocks - have dropped the sub-second remainder consistently. - Key invariant: period_displayed + cumulative_displayed >= - period_length - 1 (at most 1s truncation total). */ - scoreboard_clock_tick(11); /* total ticked = 21 */ - assert(scoreboard_game_clock_get_tenths() == 20); + /* Tick 11 more (total 21, clock_tenths = 11979). + Period 11979/10 = 1197s = 19:57. + Elapsed = (1200 - 1197) * 10 = 30 → cumulative 0:03. + 19:57 + 0:03 = 20:00 ✓ */ + scoreboard_clock_tick(11); + assert(scoreboard_game_clock_get_tenths() == 30); - /* Verify formatted strings agree: period shows 19:57, - cumulative shows 0:02. The sum (19:59) is within 1s of - period_length (20:00) due to shared truncation — but critically - they no longer disagree with each other by 1s. */ + /* Verify formatted strings: 19:57 + 0:03 = 20:00 */ char clock_buf[32], cum_buf[32]; scoreboard_clock_format(clock_buf, sizeof(clock_buf)); scoreboard_game_clock_format(cum_buf, sizeof(cum_buf)); assert(strcmp(clock_buf, "19:57") == 0); - assert(strcmp(cum_buf, "0:02") == 0); + assert(strcmp(cum_buf, "0:03") == 0); } static void test_game_clock_subsecond_sync_across_periods(void) { - /* Ensure the fix holds across period boundaries when the clock - stops mid-second at period advance. */ + /* Ensure the invariant holds across period boundaries when the + clock is mid-second at period advance. */ scoreboard_reset_state_for_tests(); scoreboard_set_game_clock_enabled(true); scoreboard_set_period_length(60); /* 1:00 periods */ scoreboard_clock_reset(); scoreboard_clock_start(); - /* Tick 5 tenths into second 1 then advance period. - Elapsed = 600 - 595 = 5 tenths → truncated to 0. - accumulated += 0 after advance. */ + /* Tick 5 tenths (clock_tenths 600 → 595). + Period displays 595/10 = 59s = 0:59. + Elapsed = (60 - 59) * 10 = 10. + Period advance accumulates 10. */ scoreboard_clock_tick(5); scoreboard_period_advance(); - /* accumulated = 0, period 2 clock reset */ - /* In period 2, tick a full 10 tenths (1 second). */ + /* Period 2: tick 10 tenths (clock_tenths 600 → 590). + Period displays 590/10 = 59s = 0:59. + Elapsed = (60 - 59) * 10 = 10. + Cumulative = 10 (accumulated) + 10 (current) = 20 → 0:02. */ scoreboard_clock_start(); scoreboard_clock_tick(10); - /* Current elapsed = 600 - 590 = 10 → (10/10)*10 = 10. */ - assert(scoreboard_game_clock_get_tenths() == 10); + assert(scoreboard_game_clock_get_tenths() == 20); } static void test_game_clock_format_mmss_over_hour(void) From 5f47ef3350706e37480cc353163e4eda533ad509 Mon Sep 17 00:00:00 2001 From: jremitz Date: Fri, 17 Apr 2026 07:31:08 -0500 Subject: [PATCH 3/4] fix: Persist cumulative game clock across OBS restarts Read cumulative_clock.txt back during scoreboard_read_all_files() so the cumulative game clock survives OBS close/reopen. Parses both M:SS and H:MM:SS formats, derives accumulated_tenths by subtracting the current-period elapsed from the total. Co-Authored-By: Claude --- src/scoreboard-core.c | 26 +++ tests/test-scoreboard-core-persistence.c | 226 +++++++++++++++++++++++ 2 files changed, 252 insertions(+) diff --git a/src/scoreboard-core.c b/src/scoreboard-core.c index 976587d..d44710f 100644 --- a/src/scoreboard-core.c +++ b/src/scoreboard-core.c @@ -165,6 +165,16 @@ static int parse_clock_text(const char *text) return -1; } +static int parse_cumulative_clock_text(const char *text) +{ + int a = 0, b = 0, c = 0; + if (sscanf(text, "%d:%d:%d", &a, &b, &c) == 3) + return (a * 3600 + b * 60 + c) * 10; /* H:MM:SS */ + if (sscanf(text, "%d:%d", &a, &b) == 2) + return (a * 60 + b) * 10; /* M:SS */ + return -1; +} + static int parse_period_text(const char *text) { /* Search labels array for an exact match */ @@ -1933,6 +1943,22 @@ bool scoreboard_read_all_files(void) g_state.period_length = val; } + /* Cumulative clock file is optional — restores game clock state. + Must come after clock.txt and period_length.txt since we need + those to compute current-period elapsed and subtract it. */ + if (g_state.game_clock_enabled && + read_text_file(dir, "cumulative_clock.txt", buf, sizeof(buf))) { + int total_tenths = parse_cumulative_clock_text(buf); + if (total_tenths >= 0) { + int current_elapsed = current_period_elapsed_tenths(); + int accumulated = total_tenths - current_elapsed; + if (accumulated < 0) + accumulated = 0; + g_state.game_clock_accumulated_tenths = accumulated; + g_state.game_clock_started = true; + } + } + g_dirty = false; return ok; } diff --git a/tests/test-scoreboard-core-persistence.c b/tests/test-scoreboard-core-persistence.c index 86d8c91..adc2fec 100644 --- a/tests/test-scoreboard-core-persistence.c +++ b/tests/test-scoreboard-core-persistence.c @@ -1422,6 +1422,226 @@ static void test_game_clock_save_load_state(void) cleanup_tmp_dir(); } +static void test_game_clock_read_all_files(void) +{ + /* Simulate OBS restart: write files, reset state, then read them + back. The cumulative clock should be restored. */ + scoreboard_reset_state_for_tests(); + setup_tmp_dir(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(900); /* 15:00 period */ + scoreboard_clock_reset(); + scoreboard_clock_start(); + scoreboard_clock_tick(900); /* 90 seconds elapsed → clock at 13:30 */ + + bool ok = scoreboard_write_all_files(); + assert(ok); + + /* Verify written cumulative_clock.txt is 1:30 */ + char path[512]; + snprintf(path, sizeof(path), "%s/cumulative_clock.txt", g_tmp_dir); + char *content = read_file_content(path); + assert(content != NULL); + assert(strcmp(content, "1:30") == 0); + free(content); + + /* Reset state (simulates OBS restart calling + scoreboard_reset_state_for_tests + scoreboard_read_all_files) */ + scoreboard_reset_state_for_tests(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(900); + scoreboard_clock_reset(); + ok = scoreboard_read_all_files(); + assert(ok); + + /* Cumulative should be restored: 1:30 = 900 tenths */ + assert(scoreboard_game_clock_get_tenths() == 900); + char gc_buf[32]; + scoreboard_game_clock_format(gc_buf, sizeof(gc_buf)); + assert(strcmp(gc_buf, "1:30") == 0); + + cleanup_tmp_dir(); +} + +static void test_game_clock_read_all_files_across_periods(void) +{ + /* Multi-period cumulative clock should survive a restart. */ + scoreboard_reset_state_for_tests(); + setup_tmp_dir(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(600); /* 10:00 periods */ + scoreboard_clock_reset(); + + /* Period 1: full 10 minutes */ + scoreboard_clock_start(); + scoreboard_clock_tick(6000); + scoreboard_period_advance(); + /* Period 2: 3 minutes elapsed (clock at 7:00) */ + scoreboard_clock_start(); + scoreboard_clock_tick(1800); + + /* Cumulative should be 13:00 = 7800 tenths */ + assert(scoreboard_game_clock_get_tenths() == 7800); + + bool ok = scoreboard_write_all_files(); + assert(ok); + + /* Reset and reload */ + scoreboard_reset_state_for_tests(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(600); + scoreboard_clock_reset(); + ok = scoreboard_read_all_files(); + assert(ok); + + assert(scoreboard_game_clock_get_tenths() == 7800); + char gc_buf[32]; + scoreboard_game_clock_format(gc_buf, sizeof(gc_buf)); + assert(strcmp(gc_buf, "13:00") == 0); + + cleanup_tmp_dir(); +} + +static void test_game_clock_read_disabled_no_restore(void) +{ + /* When game clock is disabled, cumulative_clock.txt should be + ignored even if the file exists. */ + scoreboard_reset_state_for_tests(); + setup_tmp_dir(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_clock_start(); + scoreboard_clock_tick(500); + scoreboard_write_all_files(); + + /* Reload with game clock disabled */ + scoreboard_reset_state_for_tests(); + scoreboard_set_output_directory(g_tmp_dir); + /* game_clock_enabled defaults to false */ + scoreboard_clock_reset(); + scoreboard_read_all_files(); + + assert(scoreboard_game_clock_get_tenths() == 0); + + cleanup_tmp_dir(); +} + +static void test_game_clock_read_hmmss_format(void) +{ + /* cumulative_clock.txt with H:MM:SS format should parse correctly */ + scoreboard_reset_state_for_tests(); + setup_tmp_dir(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(900); + scoreboard_clock_reset(); + + /* Write all mandatory files so read_all_files succeeds */ + scoreboard_mark_dirty(); + bool ok = scoreboard_write_all_files(); + assert(ok); + + /* Overwrite cumulative_clock.txt with H:MM:SS format */ + char path[512]; + snprintf(path, sizeof(path), "%s/cumulative_clock.txt", g_tmp_dir); + FILE *f = fopen(path, "w"); + assert(f != NULL); + fprintf(f, "1:05:30"); + fclose(f); + + /* Reset and reload */ + scoreboard_reset_state_for_tests(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(900); + scoreboard_clock_reset(); + ok = scoreboard_read_all_files(); + assert(ok); + + /* 1:05:30 = 3930 seconds = 39300 tenths. + Clock is at 15:00 (full period), so current elapsed = 0. + accumulated should be 39300. */ + assert(scoreboard_game_clock_get_tenths() == 39300); + + cleanup_tmp_dir(); +} + +static void test_game_clock_read_invalid_format(void) +{ + /* Unparseable cumulative_clock.txt should be silently ignored. */ + scoreboard_reset_state_for_tests(); + setup_tmp_dir(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_clock_reset(); + scoreboard_mark_dirty(); + scoreboard_write_all_files(); + + /* Overwrite with garbage */ + char path[512]; + snprintf(path, sizeof(path), "%s/cumulative_clock.txt", g_tmp_dir); + FILE *f = fopen(path, "w"); + assert(f != NULL); + fprintf(f, "not-a-time"); + fclose(f); + + scoreboard_reset_state_for_tests(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_clock_reset(); + scoreboard_read_all_files(); + + /* Should remain at 0 since the file was unparseable */ + assert(scoreboard_game_clock_get_tenths() == 0); + + cleanup_tmp_dir(); +} + +static void test_game_clock_read_clamps_negative(void) +{ + /* If cumulative_clock.txt has a value smaller than the current + period elapsed, accumulated should clamp to 0. */ + scoreboard_reset_state_for_tests(); + setup_tmp_dir(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(900); /* 15:00 */ + scoreboard_clock_reset(); + scoreboard_clock_start(); + scoreboard_clock_tick(3000); /* 5 minutes elapsed, clock at 10:00 */ + scoreboard_mark_dirty(); + scoreboard_write_all_files(); + + /* Overwrite cumulative_clock.txt with a value less than + the current-period elapsed (5:00 → 3000 tenths) */ + char path[512]; + snprintf(path, sizeof(path), "%s/cumulative_clock.txt", g_tmp_dir); + FILE *f = fopen(path, "w"); + assert(f != NULL); + fprintf(f, "0:30"); /* 30 seconds < 5 minutes elapsed */ + fclose(f); + + scoreboard_reset_state_for_tests(); + scoreboard_set_output_directory(g_tmp_dir); + scoreboard_set_game_clock_enabled(true); + scoreboard_set_period_length(900); + scoreboard_clock_reset(); + scoreboard_read_all_files(); + + /* accumulated would be 300 - 3000 = -2700 → clamped to 0. + get_tenths = 0 + current_elapsed (3000) = 3000. + But wait — clock.txt was written as 10:00 so clock_tenths + should be 6000. current_elapsed = (900 - 600)*10 = 3000. */ + int tenths = scoreboard_game_clock_get_tenths(); + assert(tenths == 3000); + + cleanup_tmp_dir(); +} + static void test_penalty_labels_write_files(void) { scoreboard_reset_state_for_tests(); @@ -1551,6 +1771,12 @@ int main(void) test_game_clock_write_file(); test_game_clock_no_file_when_disabled(); test_game_clock_save_load_state(); + test_game_clock_read_all_files(); + test_game_clock_read_all_files_across_periods(); + test_game_clock_read_disabled_no_restore(); + test_game_clock_read_hmmss_format(); + test_game_clock_read_invalid_format(); + test_game_clock_read_clamps_negative(); test_penalty_labels_write_files(); test_penalty_labels_write_empty(); From 234ef78768804c796aa8a0c4d1c9ee504af9ebb9 Mon Sep 17 00:00:00 2001 From: jremitz Date: Fri, 17 Apr 2026 07:31:34 -0500 Subject: [PATCH 4/4] docs: Update changelog with cumulative clock persistence fix Co-Authored-By: Claude --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ca356..99199de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ All notable changes to Streamn Scoreboard will be documented in this file. ## [0.7.1] - 2026-04-17 ### Fixed -- Cumulative game clock no longer drifts 1 second from the period clock — both clocks now truncate sub-second time consistently, so they always agree (#16) +- Cumulative game clock no longer drifts 1 second from the period clock — elapsed time is now derived from the displayed period clock, guaranteeing they always sum to the period length (#16) +- Cumulative game clock now persists across OBS restarts — previously reset to 0:00 every time OBS reopened ## [0.7.0] - 2026-04-14