Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Release

on:
push:
tags:
- "v*"

jobs:
release:
runs-on: windows-latest

permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: "pip"

- name: Install dependencies
run: pip install -r requirements.txt

- name: Resolve discord bin path
id: discord
run: |
$discordBin = python -c "import discord, os; print(os.path.join(os.path.dirname(discord.__file__), 'bin'))"
echo "bin_path=$discordBin" >> $env:GITHUB_OUTPUT
shell: pwsh

- name: Build executable
run: flet pack ./BetterCautionBot.py --hidden-import win32api --add-data "${{ steps.discord.outputs.bin_path }};." --add-data "audio;audio" -y -n BetterCautionBot --company-name Thonk --product-name BetterCautionBot

- name: Copy audio to dist
run: |
mkdir dist\audio
xcopy /s /y audio dist\audio

- name: Create release archive
working-directory: dist
run: tar -a -c -f BetterCautionBot.zip BetterCautionBot.exe audio

- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
with:
files: dist\BetterCautionBot.zip
generate_release_notes: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@
*.pyc
**/__pycache__/
.settings.json
.pytest_cache/
17 changes: 17 additions & 0 deletions modules/events/random_code_69_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,23 @@ def event_sequence(self):
leader_for_class = leader_on_track
leaders[class_] = leader_for_class

# When two cars cross the S/F line on the same tick their
# current LapDistPct has already reset to ~0, making their
# total_completed values effectively tied. Break that tie by
# the car's LapDistPct from the *previous* step (last_step) so
# the car that was physically further around the track gets
# added to the restart order first.
def _sort_key(car):
last_record = next(
(c for c in last_step if c["CarIdx"] == car["CarIdx"]), None
)
prev_dist = (
last_record["LapDistPct"] if last_record is not None else 0.0
)
return (car["total_completed"], prev_dist)

this_step = sorted(this_step, key=_sort_key, reverse=True)

for car in this_step:
existing_restart_record = [
c
Expand Down
30 changes: 22 additions & 8 deletions tests/capture_telemetry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
capture_telemetry.py -- iRacing Telemetry Capture Tool
capture_telemetry.py -- iRacing Telemetry Capture Tool (gzip-compressed output)
=======================================================

WORKFLOW
Expand All @@ -9,7 +9,7 @@
2. Run this script, passing the same Code 69 settings you use in the bot::

python tests/capture_telemetry.py \\
--output tests/fixtures/my_race.json \\
--output tests/fixtures/my_race.json.gz \\
--wave-arounds \\
--extra-lanes \\
--max-speed-km 69 \\
Expand All @@ -36,23 +36,28 @@

5. On completion two files are written side-by-side:

my_race.json telemetry frames (for ReplaySDK)
my_race.json.gz gzip-compressed telemetry frames (for ReplaySDK)
my_race.meta.json sidecar with event_kwargs + expected_restart_order

The .meta.json is immediately usable with test_fixtures.py — no manual
editing of the restart order is needed.

ReplaySDK transparently decompresses .json.gz files on load, so the test
suite needs no changes when consuming these fixtures.

SIZE NOTES
----------
- 4 Hz (not 60 Hz) : 15× fewer frames than the naïve approach
- Static keys hoisted out : removes ~70 % of per-frame payload
- Compact JSON (no indent) : 2–3× smaller than indented JSON
A 5-minute Code 69 sequence typically produces < 15 MB.
- gzip compression (level 9) : typically 5–11× further reduction
A 5-minute Code 69 sequence typically produces < 1 MB compressed.

COMMAND-LINE ARGUMENTS
----------------------
--output PATH Output telemetry file path.
Defaults to tests/fixtures/telemetry_<timestamp>.json
Defaults to tests/fixtures/telemetry_<timestamp>.json.gz
Plain .json is also accepted; ReplaySDK reads both.
--description TEXT Human-readable label for the .meta.json sidecar.
Defaults to the output filename stem.

Expand Down Expand Up @@ -91,6 +96,7 @@
from __future__ import annotations

import argparse
import gzip
import json
import queue
import sys
Expand Down Expand Up @@ -359,6 +365,8 @@ def _run_event() -> None:
event_thread.start()
print("Recording … (Ctrl-C to abort)\n")

sdk.replay_set_play_speed(1)

try:
while not recording_stop.is_set():
loop_start = time.monotonic()
Expand Down Expand Up @@ -422,8 +430,14 @@ def _run_event() -> None:
}

output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as fh:
json.dump(telemetry_output, fh, separators=(",", ":"))

# Write compressed if the path ends with .gz, otherwise plain JSON.
if output_path.suffix == ".gz":
with gzip.open(output_path, "wt", encoding="utf-8", compresslevel=9) as fh:
json.dump(telemetry_output, fh, separators=(",", ":"))
else:
with output_path.open("w", encoding="utf-8") as fh:
json.dump(telemetry_output, fh, separators=(",", ":"))

actual_size = output_path.stat().st_size
print(f"\n[DONE] {len(frames)} frames written to: {output_path}")
Expand Down Expand Up @@ -483,7 +497,7 @@ def _run_event() -> None:

def _default_output_path() -> Path:
ts = datetime.now().strftime("%Y%m%dT%H%M%S")
return Path("tests") / "fixtures" / f"telemetry_{ts}.json"
return Path("tests") / "fixtures" / f"telemetry_{ts}.json.gz"


def main(argv: list[str] | None = None) -> None:
Expand Down
26 changes: 18 additions & 8 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,12 @@ def from_meta_file(cls, meta_path: Path) -> "ReplayFixture":
"""Load a ``ReplayFixture`` from a ``.meta.json`` sidecar file.

The corresponding telemetry file is expected to sit next to the
sidecar with the same stem minus the ``.meta`` suffix::
sidecar with the same stem minus the ``.meta`` suffix. The
compressed variant (``.json.gz``) is preferred over plain ``.json``
when both exist::

tests/fixtures/my_race.json ← telemetry
tests/fixtures/my_race.json.gz ← telemetry (preferred)
tests/fixtures/my_race.json ← telemetry (fallback)
tests/fixtures/my_race.meta.json ← sidecar (this file)

Raises
Expand All @@ -179,13 +182,20 @@ def from_meta_file(cls, meta_path: Path) -> "ReplayFixture":
meta = json.load(fh)

# Derive the telemetry path: strip the ".meta" part of the stem.
# e.g. my_race.meta.json → my_race.json
telemetry_name = meta_path.name.replace(".meta.json", ".json")
telemetry_path = meta_path.parent / telemetry_name

if not telemetry_path.exists():
# e.g. my_race.meta.json → my_race.json (or my_race.json.gz)
# Prefer the compressed variant when both exist; fall back to plain JSON.
telemetry_stem = meta_path.name.replace(".meta.json", "")
gz_path = meta_path.parent / f"{telemetry_stem}.json.gz"
plain_path = meta_path.parent / f"{telemetry_stem}.json"

if gz_path.exists():
telemetry_path = gz_path
elif plain_path.exists():
telemetry_path = plain_path
else:
raise FileNotFoundError(
f"Telemetry file '{telemetry_path}' not found for sidecar '{meta_path}'."
f"Telemetry file '{plain_path}' (or '{gz_path}') not found "
f"for sidecar '{meta_path}'."
)

# Merge supplied event_kwargs over the defaults.
Expand Down
Binary file added tests/fixtures/S11R7.json.gz
Binary file not shown.
66 changes: 66 additions & 0 deletions tests/fixtures/S11R7.meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"description": "Legends RBR Late Pitters",
"event_kwargs": {
"wave_arounds": true,
"notify_on_skipped_caution": true,
"max_speed_km": 69,
"wet_speed_km": 69,
"restart_speed_pct": 125,
"reminder_frequency": 8,
"lane_names": ["One", "Two"],
"extra_lanes": true,
"auto_restart_get_ready_position": 1.8,
"auto_restart_form_lanes_position": 1.5,
"auto_class_separate_position": -1.0,
"quickie_auto_restart_get_ready_position": 0.79,
"quickie_auto_restart_form_lanes_position": 0.63,
"quickie_auto_class_separate_position": -1.0,
"quickie_window": -1,
"quickie_invert_lanes": false,
"end_of_lap_safety_margin": 0,
"max_laps_behind_leader": 99
},
"expected_restart_order": [
[
"013",
"14",
"11",
"67",
"26",
"7",
"69",
"58",
"137",
"703",
"45",
"5",
"43",
"9",
"64",
"666",
"8",
"22",
"02"
],
[
"243",
"816",
"017",
"10",
"65",
"119",
"99",
"187",
"16",
"42",
"12",
"39",
"66",
"79",
"24",
"37",
"76",
"71"
]
]
}
Binary file added tests/fixtures/bathurst.json.gz
Binary file not shown.
64 changes: 64 additions & 0 deletions tests/fixtures/bathurst.meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"description": "Bathurst",
"event_kwargs": {
"wave_arounds": true,
"notify_on_skipped_caution": true,
"max_speed_km": 69,
"wet_speed_km": 69,
"restart_speed_pct": 125,
"reminder_frequency": 8,
"lane_names": ["One", "Two"],
"extra_lanes": true,
"auto_restart_get_ready_position": 1.2,
"auto_restart_form_lanes_position": 1.1,
"auto_class_separate_position": -1.0,
"quickie_auto_restart_get_ready_position": 0.79,
"quickie_auto_restart_form_lanes_position": 0.63,
"quickie_auto_class_separate_position": -1.0,
"quickie_window": -1,
"quickie_invert_lanes": false,
"end_of_lap_safety_margin": 0,
"max_laps_behind_leader": 99
},
"expected_restart_order": [
[
"14",
"3",
"37",
"243",
"119",
"10",
"12",
"99",
"11",
"9",
"137",
"42",
"79",
"65",
"5",
"02",
"7",
"666"
],
[
"19",
"26",
"58",
"69",
"8",
"816",
"703",
"00",
"017",
"64",
"187",
"66",
"43",
"38",
"39",
"16",
"71"
]
]
}
Binary file added tests/fixtures/mugello.json.gz
Binary file not shown.
Loading